The Power of Promises: Separating UI and Business Logic in React

One of the key principles in modern frontend development is keeping UI and business logic separate. When working with async operations, the real power lies in Promises. Properly structuring Promises ensures better modularity, testability, and separation of concerns, making components easier to manage. The Problem: Mixing UI and Business Logic A common anti-pattern is placing both business logic and side effects (such as notifications) inside a custom hook: import { useMutation } from "@tanstack/react-query"; import { toast } from "react-hot-toast"; const useUpload = () => { return useMutation(async (file) => { const response = await fetch("https://api.example.com/upload", { method: "POST", body: file, }); if (!response.ok) throw new Error("Upload failed"); return response.json(); }, { onSuccess: () => toast.success("Upload successful!"), onError: () => toast.error("Upload failed!"), }); }; Here, the toast notifications are inside the hook, making it harder to control from the UI and less flexible for reuse. A Better Approach: Keeping API Handling Separate Instead of handling toasts inside the hook, we can structure our code so that business logic remains separate, making the API logic reusable without being tied to UI concerns. Step 1: Define the Business Logic const uploadFile = async (file) => { const response = await fetch("https://api.example.com/upload", { method: "POST", body: file, }); if (!response.ok) throw new Error("Upload failed"); return response.json(); }; Step 2: Use a Custom Hook to Manage API Calls (optional) import { useMutation } from "@tanstack/react-query"; const useUpload = () => { return useMutation(uploadFile); }; This keeps the API logic self-contained without enforcing any specific UI behavior. Step 3: Handle UI Logic in the Component with mutateAsync import { toast } from "react-hot-toast"; const MyComponent = () => { const { mutateAsync, isLoading } = useUpload(); const handleUpload = async (file) => { await toast.promise(mutateAsync(file), { loading: "Uploading...", success: "Upload successful!

Feb 16, 2025 - 16:05
 0
The Power of Promises: Separating UI and Business Logic in React

One of the key principles in modern frontend development is keeping UI and business logic separate. When working with async operations, the real power lies in Promises. Properly structuring Promises ensures better modularity, testability, and separation of concerns, making components easier to manage.

The Problem: Mixing UI and Business Logic

A common anti-pattern is placing both business logic and side effects (such as notifications) inside a custom hook:

import { useMutation } from "@tanstack/react-query";
import { toast } from "react-hot-toast";

const useUpload = () => {
  return useMutation(async (file) => {
    const response = await fetch("https://api.example.com/upload", {
      method: "POST",
      body: file,
    });
    if (!response.ok) throw new Error("Upload failed");
    return response.json();
  }, {
    onSuccess: () => toast.success("Upload successful!"),
    onError: () => toast.error("Upload failed!"),
  });
};

Here, the toast notifications are inside the hook, making it harder to control from the UI and less flexible for reuse.

A Better Approach: Keeping API Handling Separate

Instead of handling toasts inside the hook, we can structure our code so that business logic remains separate, making the API logic reusable without being tied to UI concerns.

Step 1: Define the Business Logic

const uploadFile = async (file) => {
  const response = await fetch("https://api.example.com/upload", {
    method: "POST",
    body: file,
  });
  if (!response.ok) throw new Error("Upload failed");
  return response.json();
};

Step 2: Use a Custom Hook to Manage API Calls (optional)

import { useMutation } from "@tanstack/react-query";

const useUpload = () => {
  return useMutation(uploadFile);
};

This keeps the API logic self-contained without enforcing any specific UI behavior.

Step 3: Handle UI Logic in the Component with mutateAsync

import { toast } from "react-hot-toast";

const MyComponent = () => {
  const { mutateAsync, isLoading } = useUpload();

  const handleUpload = async (file) => {
    await toast.promise(mutateAsync(file), {
      loading: "Uploading...",
      success: "Upload successful!