Write Clean Code using Custom Hooks in React.

ยท

5 min read

Write Clean Code using Custom Hooks in React.

After the introduction of hooks in React v16.8, the way we write code in React has changed drastically. We no longer need to write class-based components to manage state inside components. Today we will learn how to leverage custom hooks to write clean code.

Hooks helps us to handle state inside functional components.

React has many built-in hooks, like useEffect, useState, useContext. But we can also create our own custom hooks and reuse those across components.

Custom hooks are basically JavaScript functions that leverage React's built-in hooks.

The basic difference between normal JS functions and custom hook is that inside custom hook we can use React's built-in hooks to encapsulate complex logic related to state management and other side effects, but inside normal functions we can't use side effects.

As a basic naming convention custom hook should always start with the use keyword.Let's write a custom hook to demonstrate it.

// useCounter.js
import { useState } from 'react';

const useCounter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  return { count, increment };
}

export default useCounter;
// Counter.jsx
import React from 'react';
import useCounter from './useCounter';

function CounterComponent() {
  const { count, increment } = useCounter();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default CounterComponent;

Here we have encapsulated the counter logic inside this useCounter hook and now component is only responsible for rendering the UI and business logic is completed separated from the UI layer.

This way we can write code that is easy to read, understand, and maintain.

Any future changes in the counter logic won't impact the Counter.jsx component and modification can be done without worrying about the UI layer.

Let's write a more complex component where benefits of custom hook will be more visible.

import React, { useState, useRef } from 'react';

const Stopwatch = () => {
  const [isRunning, setIsRunning] = useState(false);
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    if (!isRunning) {
      setIsRunning(true);
      intervalRef.current = setInterval(() => {
        setTime((prevTime) => prevTime + 1);
      }, 1000);
    }
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    setIsRunning(false);
  };

  const reset = () => {
    clearInterval(intervalRef.current);
    setIsRunning(false);
    setTime(0);
  };

  const formatTime = (time) => {
    const hours = Math.floor(time / 3600);
    const minutes = Math.floor((time % 3600) / 60);
    const seconds = time % 60;
    return `${hours.toString().padStart(2, '0')}:${minutes
      .toString()
      .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  };

  return (
    <div>
      <h1>Stopwatch</h1>
      <p>{formatTime(time)}</p>
      <div>
        {!isRunning ? (
          <button onClick={start}>Start</button>
        ) : (
          <button onClick={stop}>Stop</button>
        )}
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}

export default Stopwatch;

Here we have written a simple Stop Watch component with Start, Stop & Reset functionality. As you can see here everything is written in the same component, which is fine in case your component is small. But as we keep on adding more functionality in our StopWatch component, component will become hard to manage.

A good way to keep your component clean and scalable is to encapsulate all your business logic inside custom hook and make your component only responsible to render the UI. This way our UI layer and business logic remains separated.

Let's refactor the above component using custom hook.We will create a useStopWatch.js custom hook and move all our state and stateful logic inside the custom hook.

// useStopWatch.js
import React, { useState, useRef } from 'react';

const useStopwatch = () => {
  const [isRunning, setIsRunning] = useState(false);
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    if (!isRunning) {
      setIsRunning(true);
      intervalRef.current = setInterval(() => {
        setTime((prevTime) => prevTime + 1);
      }, 1000);
    }
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    setIsRunning(false);
  };

  const reset = () => {
    clearInterval(intervalRef.current);
    setIsRunning(false);
    setTime(0);
  };

  return { isRunning, time, start, stop, reset };
}

Now let's import this hook inside our component and clean up the StopWatch component.

import React, { useState, useRef } from 'react';

// Hooks
import useStopwatch from './useStopwatch';

const Stopwatch = () => {
  const { isRunning, time, start, stop, reset } = useStopwatch();

  const formatTime = (time) => {
    const hours = Math.floor(time / 3600);
    const minutes = Math.floor((time % 3600) / 60);
    const seconds = time % 60;
    return `${hours.toString().padStart(2, '0')}:${minutes
      .toString()
      .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  };

  return (
    <div>
      <h1>Stopwatch</h1>
      <p>{formatTime(time)}</p>
      <div>
        {!isRunning ? (
          <button onClick={start}>Start</button>
        ) : (
          <button onClick={stop}>Stop</button>
        )}
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}

export default Stopwatch;

Component looks much more readable now and only responsible for rendering the UI part as we have encapsulate all the business logic inside the custom hook. So any future changes in the stopwatch logic doesn't impact our component at all and we can independently modify the useStopwatch hook.

Going one more step ahead, the helper function formatTime also doesn't depends on the component and it is only responsible for formatting the timer. So we can easily move this to a separate utils file and further refactor our component

// stopWatch.utils.js

export const formatTime = (time) => {
    const hours = Math.floor(time / 3600);
    const minutes = Math.floor((time % 3600) / 60);
    const seconds = time % 60;
    return `${hours.toString().padStart(2, '0')}:${minutes
      .toString()
      .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
import React, { useState, useRef } from 'react';

// Hooks
import useStopwatch from './useStopwatch';

// Utils
import { formatTime } from './stopWatch.utils';

const Stopwatch = () => {
  const { isRunning, time, start, stop, reset } = useStopwatch();

  return (
    <div>
      <h1>Stopwatch</h1>
      <p>{formatTime(time)}</p>
      <div>
        {!isRunning ? (
          <button onClick={start}>Start</button>
        ) : (
          <button onClick={stop}>Stop</button>
        )}
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}

export default Stopwatch;

As you can see now our StopWatch component has got reduced to only a few lines of code and all the business logic is handled by respective files.

This way Custom hooks can be used to abstract away complex state management or side effects, improving the readability and maintainability of components.

ย