eMoosavi
Weekly frontend dev braindumps
Decoupling Business Logic from UI with Custom React Hooks
React Components

Decoupling Business Logic from UI with Custom React Hooks

Unleash the Power of Custom Hooks to Create Modular, Reusable Logic

Jul 01, 2024 - 00:446 min read

Decoupling business logic from UI components in React applications is a vital technique for building maintainable and scalable applications. By leveraging custom hooks, you can create modular and reusable pieces of logic that stand independently of your UI components. This methodology enhances readability, encourages code reuse, and simplifies testing.

The Problem with Tangled Logic

Typically, when building React components, it's easy to mix business logic directly within the UI component. Here's a simple counter component that demonstrates this:

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

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <h1>{count}</h1>;
}

While the above code works, it entangles the timer logic directly with the UI logic. Let's start by creating a custom hook to manage this logic.

Creating the useCounter Hook

Our goal is to extract the counting logic into a reusable hook called useCounter.

import { useState, useEffect } from 'react';

function useCounter(initialValue = 0, intervalDelay = 1000) {
  const [count, setCount] = useState(initialValue);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(c => c + 1);
    }, intervalDelay);

    return () => clearInterval(interval);
  }, [intervalDelay]);

  return count;
}

With useCounter defined, our Counter component becomes significantly simpler:

import React from 'react';
import useCounter from './useCounter';

function Counter() {
  const count = useCounter();

  return <h1>{count}</h1>;
}

Managing Side Effects with Custom Hooks

Custom hooks are perfect for handling side effects. Let's create a hook that fetches user data from an API when the component mounts.

import { useState, useEffect } from 'react';

function useUserData(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const result = await response.json();
        setUser(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]);

  return { user, loading, error };
}

Now we can use this hook inside any component:

import React from 'react';
import useUserData from './useUserData';

function UserProfile({ userId }) {
  const { user, loading, error } = useUserData(userId);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

Creating Utility Hooks for Forms

Handling forms and their validations can be repetitive. Let's create a useForm hook that abstracts these concerns:

import { useState } from 'react';

function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues({ ...values, [name]: value });
    if (validate) {
      setErrors(validate(values));
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate) {
      const validationErrors = validate(values);
      setErrors(validationErrors);
      if (Object.keys(validationErrors).length === 0) {
        // Submit the form
      }
    }
  };

  return { values, errors, handleChange, handleSubmit };
}

Here's how we could use the useForm hook inside a signup component:

import React from 'react';
import useForm from './useForm';

const validate = (values) => {
  const errors = {};
  if (!values.username) errors.username = 'Username is required';
  if (!values.email) errors.email = 'Email is required';
  // additional validations
  return errors;
};

function SignupForm() {
  const { values, errors, handleChange, handleSubmit } = useForm({
    username: '',
    email: '',
    password: ''
  }, validate);

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Username</label>
        <input name="username" value={values.username} onChange={handleChange} />
        {errors.username && <p>{errors.username}</p>}
      </div>
      <div>
        <label>Email</label>
        <input name="email" value={values.email} onChange={handleChange} />
        {errors.email && <p>{errors.email}</p>}
      </div>
      <div>
        <label>Password</label>
        <input type="password" name="password" value={values.password} onChange={handleChange} />
        {errors.password && <p>{errors.password}</p>}
      </div>
      <button type="submit">Sign Up</button>
    </form>
  );
}

Abstracting Complex Logic with Hooks

Hooks provide an elegant way to encapsulate complex logic. Here, we'll create a useFetch hook for data fetching with built-in caching.

import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { cache } from 'some-cache-lib'; // Hypothetical cache library

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const cached = cache.get(url);
        if (cached) {
          setData(cached);
        } else {
          const response = await axios.get(url);
          cache.set(url, response.data);
          setData(response.data);
        }
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

Here's how to use useFetch in a component:

import React from 'react';
import useFetch from './useFetch';

function DataViewer({ apiUrl }) {
  const { data, loading, error } = useFetch(apiUrl);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Combining Multiple Hooks

Let's combine multiple custom hooks to handle complex scenarios. We'll extend the useFetch hook with pagination.

import { useState, useEffect } from 'react';
import axios from 'axios';

function usePaginatedFetch(url, itemsPerPage) {
  const [data, setData] = useState({ items: [], nextPage: 1 });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchPage = async (page) => {
    setLoading(true);
    try {
      const response = await axios.get(`${url}?page=${page}&per_page=${itemsPerPage}`);
      setData(prevData => ({
        items: [...prevData.items, ...response.data.items],
        nextPage: page + 1
      }));
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPage(1); // Initial load
  }, [url]);

  return { data, loading, error, fetchPage };
}

Here’s a component using this hook for paginated data fetching:

import React from 'react';
import usePaginatedFetch from './usePaginatedFetch';

function PaginatedList({ apiUrl }) {
  const { data, loading, error, fetchPage } = usePaginatedFetch(apiUrl, 10);

  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <ul>
        {data.items.map((item, index) => (
          <li key={index}>{item.name}</li>
        ))}
      </ul>
      {loading ? (
        <div>Loading...</div>
      ) : (
        <button onClick={() => fetchPage(data.nextPage)}>Load More</button>
      )}
    </div>
  );
}

Conclusion

Custom hooks in React are a powerful tool for separating business logic from UI presentation. By creating reusable, composable hooks, you can achieve more modular and maintainable code. Whether handling side effects, managing forms, or fetching data – custom hooks help keep your components clean and focused on rendering UI.

Hooks empower developers to encapsulate systemic concerns while maintaining readability and reusability. Start incorporating custom hooks into your projects today and experience the enhanced clarity and modularity in your codebase.

Article tags
reacthookscustom-hookscomponent-designbusiness-logic
Previous article

React Components

Crafting Polymorphic Components in React

Next article

Web Performance

Leveraging Intersection Observer API with React for Enhanced Performance