راهنمای عملی برنامه‌نویسی

از کد کثیف تا کد تمیز؛ چطور یک کامپوننت شلوغ را نجات دهیم؟

فرض کن یک کامپوننت React ساخته‌ای. اول همه‌چیز مرتب بوده، اما با اضافه شدن قابلیت‌های جدید، کدها شلوغ شده‌اند. حالا پیدا کردن یک باگ سخت است و تغییر یک قسمت ممکن است قسمت دیگری را خراب کند. از کجا باید شروع کنی؟

موضوع: Clean Code، Refactoring، React و TypeScript

۱. اصلاً کد کثیف یعنی چه؟

کد کثیف الزاماً کدی نیست که اجرا نمی‌شود؛ گاهی کد کاملاً درست کار می‌کند، اما فهمیدن، تغییر دادن و توسعه دادن آن سخت است.

تصور کن یک اتاق داری. در ابتدا فقط یک میز و صندلی داخل آن است. اما کم‌کم لباس‌ها، کتاب‌ها، ابزارها و وسایل مختلف را بدون نظم در همان اتاق می‌گذاری. هنوز می‌توانی از اتاق استفاده کنی، ولی پیدا کردن هر وسیله سخت شده است.

کد هم همین‌طور است. ممکن است همه‌چیز داخل یک کامپوننت قرار گرفته باشد: نمایش فرم، اعتبارسنجی، ارسال درخواست، مدیریت خطا، نمایش پیام موفقیت، فیلتر کردن داده‌ها و حتی محاسبه قیمت.

یک مثال ساده

فرض کن یک کامپوننت برای ثبت‌نام کاربر نوشته‌ای:

نسخه شلوغ React + JavaScript
function Register() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  async function handleSubmit(event) {
    event.preventDefault();

    if (name.trim() === "") {
      setError("نام را وارد کن");
      return;
    }

    if (!email.includes("@")) {
      setError("ایمیل معتبر نیست");
      return;
    }

    if (password.length < 8) {
      setError("رمز عبور باید حداقل ۸ کاراکتر باشد");
      return;
    }

    setLoading(true);
    setError("");

    try {
      const response = await fetch("/api/register", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          name,
          email,
          password
        })
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.message || "ثبت‌نام ناموفق بود");
      }

      alert("ثبت‌نام موفق بود");
    } catch (error) {
      setError(error.message || "خطایی رخ داد");
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(event) => setName(event.target.value)}
        placeholder="نام"
      />

      <input
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        placeholder="ایمیل"
      />

      <input
        type="password"
        value={password}
        onChange={(event) => setPassword(event.target.value)}
        placeholder="رمز عبور"
      />

      <button disabled={loading}>
        {loading ? "در حال ثبت‌نام..." : "ثبت‌نام"}
      </button>

      {error && <p>{error}</p>}
    </form>
  );
}

این کد برای یک مثال کوچک قابل فهم است و لزوماً کد بدی محسوب نمی‌شود. مشکل زمانی شروع می‌شود که قابلیت‌های زیادی به آن اضافه کنیم.

مثلاً بعداً بخواهیم این موارد را هم اضافه کنیم:

  • بررسی تکراری نبودن ایمیل.
  • نمایش قوانین رمز عبور.
  • نمایش پیام‌های مختلف سرور.
  • قابلیت نمایش و مخفی کردن رمز عبور.
  • ذخیره اطلاعات در چند مرحله.
  • ارسال کد تأیید ایمیل.

حالا ممکن است کامپوننت به صدها خط کد برسد. هرچقدر قابلیت بیشتری اضافه کنیم، پیدا کردن محل درست برای تغییر سخت‌تر می‌شود.

نکته مهم تعداد خطوط زیاد به‌تنهایی نشانه کد بد نیست. مشکل اصلی، پیچیدگی غیرضروری، مسئولیت‌های نامرتبط، تکرار منطق و سخت شدن تغییرات است.

۲. بازآرایی کد (Refactoring) چیست؟

بازآرایی یعنی ساختار داخلی کد را بهتر کنیم، بدون اینکه رفتار قابل مشاهده برنامه را تغییر دهیم.

به زبان ساده، می‌خواهیم همان نتیجه قبلی را بگیریم، اما با کدی که فهمیدن و توسعه دادن آن آسان‌تر باشد.

یک مثال خارج از برنامه‌نویسی

فرض کن تمام وسایل آشپزخانه را در یک کشو ریخته‌ای. برای مرتب کردن آن‌ها، وسایل را بر اساس کاربرد در کشوهای جداگانه قرار می‌دهی.

تعداد وسایل تغییر نکرده است؛ کاربرد آشپزخانه هم تغییر نکرده است. فقط ساختار بهتر شده و پیدا کردن هر وسیله آسان‌تر است.

قبل از بازآرایی

function calculate() {
  // بررسی اطلاعات
  // محاسبه قیمت
  // محاسبه تخفیف
  // محاسبه مالیات
  // نمایش نتیجه
}

بعد از بازآرایی

function calculate() {
  const subtotal = calculateSubtotal();
  const discount = calculateDiscount(subtotal);
  const tax = calculateTax(subtotal);

  return subtotal - discount + tax;
}

در نسخه دوم، مراحل محاسبه مشخص هستند. برای فهمیدن جزئیات تخفیف، فقط لازم است تابع مربوط به تخفیف را بخوانیم.

بازآرایی با بازنویسی کامل فرق دارد. بازنویسی ممکن است از ابتدا طراحی و پیاده‌سازی شود. اما در بازآرایی، ترجیح می‌دهیم با قدم‌های کوچک و قابل بررسی، ساختار فعلی را بهبود بدهیم.

۳. قبل از تغییر کد چه کار کنیم؟

یکی از اشتباهات رایج این است که برنامه‌نویس بلافاصله شروع به انتقال کدها به فایل‌های مختلف می‌کند. اما هنوز نمی‌داند کدام قسمت مشکل دارد و چگونه مطمئن شود تغییرات باعث خراب شدن برنامه نشده‌اند.

قدم اول: رفتار فعلی را بشناس

فرض کن کامپوننتی برای ویرایش مقاله داری. قبل از بازآرایی، مشخص کن که:

  • اطلاعات مقاله چگونه بارگذاری می‌شوند.
  • کاربر چگونه متن مقاله را تغییر می‌دهد.
  • تصویر چگونه اضافه یا حذف می‌شود.
  • هنگام ذخیره چه درخواستی ارسال می‌شود.
  • خطاها چگونه نمایش داده می‌شوند.

اگر این رفتارها را نشناسی، ممکن است هنگام مرتب‌سازی کد، قابلیت حذف تصویر یا ذخیره مقاله را ناخواسته خراب کنی.

قدم دوم: از تست استفاده کن

اگر پروژه تست خودکار دارد، قبل از تغییرات آن‌ها را اجرا کن. اگر ندارد، حداقل رفتارهای اصلی را به‌صورت دستی بررسی کن. برای بخش‌های حساس بهتر است تست اضافه کنی.

مثال یک تست ساده Vitest + React Testing Library
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Register from "./Register";

describe("Register", () => {
  it("دکمه ثبت‌نام را نمایش می‌دهد", () => {
    render(<Register />);

    expect(
      screen.getByRole("button", { name: "ثبت‌نام" })
    ).toBeInTheDocument();
  });
});

این تست فقط وجود دکمه را بررسی می‌کند. برای اطمینان از ثبت‌نام واقعی، باید تست‌های بیشتری برای اعتبارسنجی، ارسال درخواست و نمایش خطا بنویسی.

اگر از این ابزارها استفاده می‌کنی، مطمئن شو وابستگی‌های لازم، از جمله تطبیق‌دهنده‌های مربوط به toBeInTheDocument، نصب و پیکربندی شده باشند.

قدم سوم: از Git کمک بگیر

اگر پروژه تحت کنترل نسخه است، ابتدا وضعیت تغییرات را بررسی کن:

git status
git diff

با این کار می‌فهمی چه تغییراتی از قبل وجود دارند. قبل از شروع بازآرایی، تغییرات مهم را ثبت کن؛ البته اگر کارهای ناتمام داری، آن‌ها را بدون بررسی پاک یا بازنویسی نکن.

قانون طلایی اول رفتار فعلی را بشناس، بعد کد را تغییر بده و پس از هر تغییر مهم، دوباره تست کن.

۴. نام‌گذاری درست؛ اولین قدم برای خوانایی

نام متغیر یا تابع باید تا حد امکان توضیح بدهد که آن متغیر چه چیزی را نگه می‌دارد یا آن تابع چه کاری انجام می‌دهد.

فرض کن وارد یک انبار شده‌ای و روی جعبه‌ها فقط نوشته شده «جعبه ۱»، «جعبه ۲» و «جعبه ۳». برای پیدا کردن وسیله مورد نظرت باید همه جعبه‌ها را باز کنی. نام‌گذاری ضعیف در کد نیز همین مشکل را ایجاد می‌کند.

نام مبهم

const x = 120;
const d = 0.15;
const r = x - x * d;

نام معنادار

const price = 120;
const discountRate = 0.15;

const finalPrice =
  price - price * discountRate;

در نسخه دوم، بدون اینکه کامنتی بخوانیم، متوجه می‌شویم قیمت اولیه، نرخ تخفیف و قیمت نهایی چه معنایی دارند.

نام‌گذاری تابع‌ها

نام تابع بهتر است کاری را که انجام می‌دهد مشخص کند. در JavaScript و TypeScript، نام‌هایی مانند validateEmail، formatDate و calculateTotal معمولاً از نام‌های مبهم بهترند.

نام نامناسب

function doIt(user) {
  return user.age >= 18;
}

نام مناسب‌تر

function isAdult(user) {
  return user.age >= 18;
}

نام بولین‌ها

متغیرهایی که مقدار درست یا غلط دارند، بهتر است طوری نام‌گذاری شوند که خواندن شرط را ساده کنند.

const isLoading = true;
const hasError = false;
const canEditArticle = true;

if (isLoading) {
  // نمایش وضعیت بارگذاری
}

لازم نیست برای همه متغیرها نام‌های طولانی انتخاب کنی. نام خوب، نامی است که در همان محدوده اطلاعات کافی به خواننده می‌دهد.

۵. توابع طولانی را چگونه اصلاح کنیم؟

یکی از نشانه‌های رایج کد دشوار، تابعی است که کارهای زیادی انجام می‌دهد. مثلاً اطلاعات را اعتبارسنجی می‌کند، از سرور داده می‌گیرد، قیمت را محاسبه می‌کند و در پایان رابط کاربری را تغییر می‌دهد.

البته هر تابع طولانی الزاماً بد نیست؛ مهم این است که آیا مراحل آن مسئولیت‌های جداگانه دارند یا نه.

مثال: محاسبه قیمت سفارش

نسخه اول مسئولیت‌های درهم‌آمیخته
function getOrderTotal(items) {
  let total = 0;

  for (const item of items) {
    if (item.price < 0 || item.quantity < 0) {
      throw new Error("اطلاعات سفارش معتبر نیست");
    }

    total += item.price * item.quantity;
  }

  if (total > 1000) {
    total = total * 0.9;
  }

  total = total * 1.09;

  return total;
}

این تابع در مثال ما سه کار انجام می‌دهد:

  1. بررسی معتبر بودن اطلاعات.
  2. محاسبه مبلغ کالاها و تخفیف.
  3. محاسبه مالیات.

می‌توانیم این مراحل را به توابع مستقل تبدیل کنیم تا هرکدام نام مشخصی داشته باشند.

نسخه بازآرایی‌شده JavaScript
function validateItems(items) {
  for (const item of items) {
    if (item.price < 0 || item.quantity < 0) {
      throw new Error("اطلاعات سفارش معتبر نیست");
    }
  }
}

function calculateSubtotal(items) {
  return items.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
}

function applyDiscount(subtotal) {
  if (subtotal > 1000) {
    return subtotal * 0.9;
  }

  return subtotal;
}

function calculateTax(amount) {
  return amount * 0.09;
}

function getOrderTotal(items) {
  validateItems(items);

  const subtotal = calculateSubtotal(items);
  const discountedTotal = applyDiscount(subtotal);
  const tax = calculateTax(discountedTotal);

  return discountedTotal + tax;
}

حالا تابع اصلی شبیه یک دستورالعمل است: اعتبارسنجی کن، مبلغ را محاسبه کن، تخفیف را اعمال کن، مالیات را حساب کن و نتیجه را برگردان.

یک نکته مهم درباره مثال این مثال برای آموزش ساختار کد است. در پروژه واقعی باید مشخص کنی مالیات قبل از تخفیف محاسبه می‌شود یا بعد از آن، مقادیر عددی چگونه گرد می‌شوند و اطلاعات ورودی چه محدودیت‌هایی دارند. بازآرایی نباید ناخواسته قواعد کسب‌وکار را تغییر دهد.

آیا باید هر چند خط را به یک تابع تبدیل کنیم؟

خیر. هدف این نیست که برای هر عملیات کوچک یک تابع جدا بسازیم. اگر تابعی کوتاه، واضح و دارای یک مسئولیت مشخص است، ممکن است نیازی به تغییر نداشته باشد.

تابع جداگانه زمانی مفیدتر است که:

  • یک مسئولیت مشخص و قابل نام‌گذاری دارد.
  • در چند جای برنامه استفاده می‌شود.
  • منطق پیچیده‌ای دارد که بهتر است جداگانه تست شود.
  • جدا شدن آن باعث می‌شود تابع اصلی خواناتر شود.

۶. شرط‌های پیچیده را ساده کنیم

شرط‌ها بخشی طبیعی از برنامه‌نویسی هستند؛ اما وقتی تعداد زیادی شرط تو در تو داشته باشیم، فهمیدن مسیر اجرای برنامه سخت می‌شود.

مثال اول: شرط‌های تو در تو

نسخه پیچیده

function canPublish(user, article) {
  if (user) {
    if (user.isActive) {
      if (user.role === "admin") {
        if (article.title) {
          return true;
        }
      }
    }
  }

  return false;
}

نسخه خواناتر

function canPublish(user, article) {
  if (!user) return false;
  if (!user.isActive) return false;
  if (user.role !== "admin") return false;
  if (!article.title) return false;

  return true;
}

نسخه دوم از روشی به نام Early Return استفاده می‌کند. یعنی اگر شرط لازم برقرار نبود، زودتر از تابع خارج می‌شویم.

به این ترتیب، لازم نیست برای فهمیدن نتیجه، تمام لایه‌های شرط را دنبال کنیم.

توجه به امنیت این مثال فقط یک بررسی ساده است. در برنامه واقعی، مجوز انتشار مقاله باید در سمت سرور نیز بررسی شود. مخفی کردن یک دکمه در React جایگزین کنترل دسترسی سمت سرور نیست.

مثال دوم: استخراج شرط پیچیده به متغیر

شرط دشوار برای خواندن

if (
  user &&
  user.isActive &&
  user.age >= 18 &&
  !user.isBlocked
) {
  // ادامه عملیات
}

نام‌گذاری منطق

const canAccess = Boolean(
  user &&
  user.isActive &&
  user.age >= 18 &&
  !user.isBlocked
);

if (canAccess) {
  // ادامه عملیات
}

با نام‌گذاری شرط، خواننده سریع‌تر متوجه می‌شود این بررسی برای چه هدفی انجام می‌شود. اگر منطق در چند جای برنامه استفاده می‌شود، می‌توان آن را به تابعی مستقل تبدیل کرد.

مثال سوم: انتخاب بین چند وضعیت

فرض کن می‌خواهی وضعیت یک مقاله را نمایش بدهی:

function getArticleStatus(status) {
  if (status === "draft") {
    return "پیش‌نویس";
  }

  if (status === "published") {
    return "منتشر شده";
  }

  if (status === "archived") {
    return "بایگانی شده";
  }

  return "وضعیت نامشخص";
}

برای چند حالت محدود، این روش خواناست. اگر تعداد حالت‌ها زیاد شد، می‌توانی از یک شیء برای نگاشت وضعیت به متن استفاده کنی:

const statusLabels = {
  draft: "پیش‌نویس",
  published: "منتشر شده",
  archived: "بایگانی شده"
};

function getArticleStatus(status) {
  return statusLabels[status] ?? "وضعیت نامشخص";
}

این روش زمانی مناسب است که هر وضعیت فقط به یک متن ثابت تبدیل شود. اگر هر حالت رفتار پیچیده و متفاوتی داشته باشد، شرط‌ها یا switch ممکن است انتخاب مناسب‌تری باشند.

۷. کدهای تکراری را حذف کنیم

فرض کن برای نمایش تاریخ در سه بخش مختلف پروژه، سه بار یک منطق مشابه نوشته‌ای. اگر بعداً بخواهی قالب تاریخ را تغییر بدهی، باید هر سه قسمت را پیدا کنی و اصلاح کنی.

به این مشکل Code Duplication می‌گوییم؛ یعنی تکرار منطق یا کد.

مثال: قالب‌بندی تاریخ

کد تکراری

const date1 = new Date(article.createdAt)
  .toLocaleDateString("fa-IR");

const date2 = new Date(comment.createdAt)
  .toLocaleDateString("fa-IR");

const date3 = new Date(user.createdAt)
  .toLocaleDateString("fa-IR");

تابع مشترک

function formatDate(date) {
  return new Date(date)
    .toLocaleDateString("fa-IR");
}

const date1 = formatDate(article.createdAt);
const date2 = formatDate(comment.createdAt);
const date3 = formatDate(user.createdAt);

حالا اگر قالب نمایش تاریخ تغییر کند، فقط تابع formatDate را اصلاح می‌کنیم.

مثال در React

فرض کن در چند قسمت، یک دکمه با ظاهر مشابه داری:

function SaveButton({ onClick, loading }) {
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={loading}
    >
      {loading ? "در حال ذخیره..." : "ذخیره"}
    </button>
  );
}

حالا می‌توانی این دکمه را در چند بخش استفاده کنی. اگر ظاهر یا رفتار عمومی آن تغییر کند، یک محل مشخص برای اصلاح داری.

هر تکراری را حذف نکن! اگر دو قطعه کد فقط از نظر ظاهری شبیه‌اند اما قواعد متفاوتی دارند، ادغام اجباری آن‌ها ممکن است پیچیدگی بیشتری ایجاد کند. بهتر است منطق مشترک واقعی را استخراج کنی، نه اینکه صرفاً کدهای شبیه را به هر قیمتی یکی کنی.

۸. چه زمانی کامپوننت را تقسیم کنیم؟

در React، یک کامپوننت می‌تواند رابط کاربری را نمایش بدهد، وضعیت محلی را مدیریت کند و رویدادها را کنترل کند. اما وقتی چند بخش مستقل با وظایف متفاوت در یک کامپوننت جمع می‌شوند، بهتر است ساختار آن را بررسی کنیم.

مثال: صفحه مدیریت مقاله

فرض کن کامپوننتی داری که همه این موارد را مدیریت می‌کند:

  • فرم عنوان و توضیحات مقاله.
  • ویرایشگر متن مقاله.
  • آپلود تصاویر.
  • انتخاب دسته‌بندی.
  • نمایش پیش‌نمایش مقاله.
  • دکمه انتشار و ذخیره پیش‌نویس.

اگر همه این قسمت‌ها در یک فایل بزرگ قرار گرفته باشند، پیدا کردن خطای مربوط به آپلود تصویر ممکن است به بررسی بخش‌های نامرتبط نیاز داشته باشد.

می‌توانیم ساختار را به شکل زیر تقسیم کنیم:

ArticleEditor
├── ArticleForm
│   ├── ArticleTitleInput
│   └── CategorySelect
├── ArticleContentEditor
├── ArticleImageUploader
├── ArticlePreview
└── ArticleActions

این ساختار صرفاً یک مثال است؛ لازم نیست برای هر ورودی یا هر تکه JSX یک فایل مستقل بسازی. تقسیم‌بندی باید به خوانایی و نگهداری کمک کند.

مثال عملی

فرض کن پیش‌نمایش مقاله در کامپوننت اصلی نوشته شده است:

قبل همه‌چیز در یک کامپوننت
function ArticleEditor({ article }) {
  const [title, setTitle] = useState(article.title);
  const [content, setContent] = useState(article.content);

  return (
    <div>
      <input
        value={title}
        onChange={(event) => setTitle(event.target.value)}
      />

      <textarea
        value={content}
        onChange={(event) => setContent(event.target.value)}
      />

      <section>
        <h2>{title}</h2>
        <p>{content}</p>
      </section>
    </div>
  );
}

می‌توانیم پیش‌نمایش را به یک کامپوننت مستقل تبدیل کنیم:

ArticlePreview.jsx کامپوننت مستقل
function ArticlePreview({ title, content }) {
  return (
    <section>
      <h2>{title || "عنوان مقاله"}</h2>
      <p>{content || "متن مقاله اینجا نمایش داده می‌شود."}</p>
    </section>
  );
}

export default ArticlePreview;

حالا کامپوننت اصلی خواناتر می‌شود:

function ArticleEditor({ article }) {
  const [title, setTitle] = useState(article.title);
  const [content, setContent] = useState(article.content);

  return (
    <div>
      <input
        value={title}
        onChange={(event) => setTitle(event.target.value)}
      />

      <textarea
        value={content}
        onChange={(event) => setContent(event.target.value)}
      />

      <ArticlePreview
        title={title}
        content={content}
      />
    </div>
  );
}

اکنون پیش‌نمایش مسئول نمایش محتوا است و کامپوننت اصلی مسئول نگهداری وضعیت و هماهنگی بخش‌هاست.

چه زمانی تقسیم کامپوننت مفید است؟

  • یک بخش از رابط کاربری منطق مشخص و مستقلی دارد.
  • یک بخش در چند جای پروژه قابل استفاده است.
  • تغییرات یک بخش معمولاً مستقل از بخش‌های دیگر است.
  • کامپوننت اصلی برای فهمیدن یک قابلیت ساده، نیاز به خواندن کد زیادی دارد.

اما اگر کامپوننت فرزند فقط چند خط ساده دارد و جدا کردن آن باعث می‌شود تعداد زیادی پراپ بی‌دلیل بین فایل‌ها جابه‌جا شوند، ممکن است تقسیم کردن کمکی نکند.

۹. مدیریت بهتر State و Hookها

یکی از مشکلات رایج در کامپوننت‌های بزرگ React، تعداد زیادی متغیر وضعیت (State) و منطق وابسته به آن‌هاست.

مثلاً در یک فرم ممکن است این وضعیت‌ها را داشته باشیم:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [showPassword, setShowPassword] = useState(false);

تعداد Stateها به‌تنهایی مشکل نیست. سؤال مهم این است که آیا هر وضعیت واقعاً باید جداگانه ذخیره شود یا می‌توان آن را از وضعیت دیگری محاسبه کرد.

مثال: وضعیت قابل محاسبه

ذخیره وضعیت اضافی

const [password, setPassword] = useState("");
const [isPasswordValid, setIsPasswordValid] =
  useState(false);

// باید هر بار هر دو را هماهنگ کنیم.

محاسبه از روی داده

const [password, setPassword] = useState("");

const isPasswordValid =
  password.length >= 8;

در مثال دوم، هر بار که رمز تغییر کند، مقدار اعتبار آن هم به‌صورت خودکار از روی رمز جدید محاسبه می‌شود. دیگر نیازی نیست دو State را هم‌زمان به‌روزرسانی کنیم.

البته اگر اعتبار رمز به قواعد پیچیده‌تری وابسته باشد، می‌توانیم یک تابع مستقل برای اعتبارسنجی بنویسیم.

چه زمانی از Custom Hook استفاده کنیم؟

اگر منطق مربوط به State و رفتار کامپوننت پیچیده شده باشد و احتمال استفاده مجدد از آن وجود داشته باشد، می‌توانیم یک Custom Hook بسازیم.

مثلاً مدیریت وضعیت باز و بسته بودن یک پنجره:

import { useState } from "react";

function useDisclosure(initialValue = false) {
  const [isOpen, setIsOpen] = useState(initialValue);

  function open() {
    setIsOpen(true);
  }

  function close() {
    setIsOpen(false);
  }

  function toggle() {
    setIsOpen((current) => !current);
  }

  return {
    isOpen,
    open,
    close,
    toggle
  };
}

حالا در کامپوننت از آن استفاده می‌کنیم:

function HelpDialog() {
  const dialog = useDisclosure();

  return (
    <div>
      <button onClick={dialog.open}>
        راهنما
      </button>

      {dialog.isOpen && (
        <div>
          <p>راهنمای استفاده از برنامه</p>
          <button onClick={dialog.close}>
            بستن
          </button>
        </div>
      )}
    </div>
  );
}

اکنون منطق باز و بسته شدن پنجره در یک محل قرار دارد و می‌تواند در کامپوننت‌های مختلف استفاده شود.

هر منطق کوچکی نیاز به Custom Hook ندارد. اگر استخراج منطق به خوانایی کمک نمی‌کند یا فقط یک خط ساده را جابه‌جا می‌کند، بهتر است بی‌دلیل پیچیدگی جدید نسازی.

۱۰. جدا کردن منطق درخواست‌های API

فرض کن در چند کامپوننت، مستقیماً از fetch برای دریافت و ارسال اطلاعات استفاده می‌کنی. هر کامپوننت هم باید آدرس، هدرها، نحوه بررسی خطا و تبدیل پاسخ را مدیریت کند.

اگر روش ارتباط با سرور تغییر کند، ممکن است مجبور شوی تعداد زیادی فایل را اصلاح کنی.

نسخه‌ای که تکرار زیادی دارد

async function loadArticles() {
  const response = await fetch("/api/articles");
  const data = await response.json();

  if (!response.ok) {
    throw new Error("دریافت مقاله‌ها ناموفق بود");
  }

  return data;
}

async function loadUsers() {
  const response = await fetch("/api/users");
  const data = await response.json();

  if (!response.ok) {
    throw new Error("دریافت کاربران ناموفق بود");
  }

  return data;
}

در این مثال، نحوه بررسی پاسخ در هر دو تابع مشابه است. می‌توانیم یک تابع عمومی برای درخواست‌ها ایجاد کنیم.

apiClient.js تابع مشترک
async function request(url, options = {}) {
  const response = await fetch(url, options);

  const data = await response.json().catch(() => null);

  if (!response.ok) {
    throw new Error(
      data?.message || "درخواست ناموفق بود"
    );
  }

  return data;
}

export function getArticles() {
  return request("/api/articles");
}

export function getUsers() {
  return request("/api/users");
}

اکنون کامپوننت می‌تواند به جای درگیر شدن با جزئیات درخواست، فقط تابع مناسب را صدا بزند.

async function loadArticles() {
  try {
    const articles = await getArticles();
    setArticles(articles);
  } catch (error) {
    setError(error.message);
  }
}

این نمونه فرض می‌کند پاسخ سرور دارای بدنه JSON است یا بدنه خالی را می‌توان مدیریت کرد. اگر API پاسخ‌هایی مانند 204 No Content یا متن ساده برگرداند، باید کلاینت را متناسب با قرارداد API تنظیم کنی.

ساختار پیشنهادی فایل‌ها

src/
├── components/
│   ├── ArticleList.jsx
│   └── ArticleCard.jsx
├── services/
│   ├── apiClient.js
│   └── articleService.js
├── hooks/
│   └── useArticles.js
└── pages/
    └── ArticlesPage.jsx

این ساختار یک پیشنهاد است، نه قانون اجباری. در پروژه‌های کوچک ممکن است فایل‌های کمتر کافی باشند. مهم این است که مرز مسئولیت‌ها واضح باشد.

نکته امنیتی جدا کردن درخواست‌های API به‌تنهایی امنیت ایجاد نمی‌کند. اعتبارسنجی ورودی، احراز هویت و مجوز دسترسی باید در سمت سرور نیز انجام شوند. اطلاعات محرمانه مانند کلیدهای خصوصی سرور نباید در کد فرانت‌اند قرار بگیرند.

۱۱. استفاده درست از TypeScript

TypeScript کمک می‌کند شکل داده‌ها و قراردادهای بین توابع و کامپوننت‌ها واضح‌تر باشند. این موضوع در بازآرایی اهمیت زیادی دارد؛ چون هنگام جابه‌جایی منطق، متوجه بسیاری از اشتباهات مربوط به نوع داده می‌شویم.

مثال: پراپ‌های بدون نوع مشخص

تعریف مبهم

function ArticleCard(props: any) {
  return <h2>{props.title}</h2>;
}

تعریف مشخص

type ArticleCardProps = {
  title: string;
  author: string;
  published: boolean;
};

function ArticleCard({
  title,
  author,
  published
}: ArticleCardProps) {
  return (
    <article>
      <h2>{title}</h2>
      <p>{author}</p>
      <p>{published ? "منتشر شده" : "پیش‌نویس"}</p>
    </article>
  );
}

در نسخه دوم، مشخص است که کامپوننت چه اطلاعاتی دریافت می‌کند. اگر اشتباهاً مقدار بولین را به شکل متن ارسال کنیم، TypeScript می‌تواند خطا بدهد.

مثال: جلوگیری از مقادیر نامعتبر

فرض کن وضعیت مقاله فقط می‌تواند یکی از سه مقدار مشخص باشد:

type ArticleStatus =
  | "draft"
  | "published"
  | "archived";

type Article = {
  id: number;
  title: string;
  status: ArticleStatus;
};

حالا TypeScript اجازه نمی‌دهد به‌راحتی وضعیت نامعتبر مانند "deleted" را به این نوع اختصاص بدهی.

البته TypeScript به‌تنهایی اطلاعات دریافتی از سرور را اعتبارسنجی نمی‌کند. داده‌های خارجی همچنان باید در زمان اجرا بررسی شوند؛ برای مثال با یک اعتبارسنجی مناسب یا کتابخانه‌ای مانند Zod.

۱۲. اشتباهات رایج هنگام تمیزکاری کد

اشتباه اول: بازنویسی کامل در یک مرحله

فرض کن یک کامپوننت ۶۰۰ خطی داری و تصمیم می‌گیری تمام آن را از اول بنویسی. در این صورت ممکن است رفتارهای خاص، رفع باگ‌های قبلی یا محدودیت‌های مهم برنامه را فراموش کنی.

راه بهتر این است که هر بار یک بخش مشخص را تغییر بدهی و بعد آن را بررسی کنی.

اشتباه دوم: ساختن فایل‌های بیش از حد

داشتن ۵۰ فایل کوچک لزوماً بهتر از داشتن ۱۰ فایل مرتب نیست. اگر برای هر عملیات یک فایل مستقل بسازی، دنبال کردن مسیر اجرای برنامه سخت‌تر می‌شود.

فایل جدید زمانی مفید است که مسئولیت مشخصی داشته باشد یا جداسازی آن ساختار برنامه را واضح‌تر کند.

اشتباه سوم: استفاده بیش از حد از Abstraction

Abstraction یعنی جزئیات غیرضروری را پشت یک رابط ساده‌تر پنهان کنیم. این کار مفید است، اما اگر برای یک عملیات ساده چندین لایه تابع و کلاس ایجاد کنیم، ممکن است فهمیدن کد سخت‌تر شود.

// برای یک عملیات ساده، این تابع کافی است.
function double(number) {
  return number * 2;
}

لازم نیست برای چنین عملیات کوچکی، یک کلاس، یک سرویس و چندین لایه واسطه بسازی؛ مگر اینکه نیاز واقعی پروژه چنین ساختاری را توجیه کند.

اشتباه چهارم: حذف کامنت‌ها بدون بررسی

کامنتی که فقط کد را به زبان فارسی یا انگلیسی تکرار می‌کند، معمولاً ارزش زیادی ندارد. اما کامنتی که یک تصمیم مهم، محدودیت فنی یا دلیل غیرآشکار را توضیح می‌دهد، می‌تواند بسیار مفید باشد.

کامنت کم‌ارزش

// افزایش تعداد
count++;

کامنت مفید

// برای جلوگیری از ارسال دوباره درخواست
// هنگام دوبار کلیک کاربر
if (isSubmitting) return;

کامنت دوم فقط زمانی مفید است که واقعاً رفتار سیستم را توضیح دهد؛ نام متغیرها و ساختار کد نیز باید تا حد ممکن واضح باشند.

اشتباه پنجم: تغییر هم‌زمان منطق و ظاهر

اگر هم‌زمان ساختار کامپوننت، طراحی رابط کاربری، نحوه ارسال درخواست و قواعد اعتبارسنجی را تغییر بدهی، هنگام بروز خطا سخت‌تر می‌توانی بفهمی مشکل از کدام تغییر بوده است.

بهتر است بازآرایی ساختاری را از تغییرات ظاهری و تغییر قابلیت‌ها جدا نگه داری.

اشتباه ششم: تصور اینکه کوتاه‌تر یعنی بهتر

کوتاه اما مبهم

const x = a.filter(
  i => i.a && !i.b
);

واضح‌تر

const activeItems = items.filter(
  item => item.isActive && !item.isDeleted
);

خوانایی از کم کردن تعداد خطوط مهم‌تر است. قرار نیست کد را به معمایی تبدیل کنیم که فقط نویسنده آن بتواند بفهمد.

۱۳. فرایند عملی بازآرایی از ابتدا تا انتها

حالا فرض کنیم در پروژه واقعی خودت یک کامپوننت شلوغ پیدا کرده‌ای. به‌جای اینکه همه‌چیز را یک‌باره تغییر بدهی، این مسیر را دنبال کن.

  1. رفتار فعلی را ثبت کن.

    مشخص کن کامپوننت چه ورودی‌هایی می‌گیرد، چه چیزی نمایش می‌دهد، چه درخواست‌هایی می‌فرستد و چه اتفاق‌هایی باید رخ دهند.

  2. تست‌ها و وضعیت Git را بررسی کن.

    تست‌های موجود را اجرا کن و تغییرات قبلی پروژه را بررسی کن. اگر تستی وجود ندارد، برای رفتارهای مهم تست اضافه کن یا بررسی دستی دقیقی انجام بده.

  3. بخش‌های مسئله‌دار را مشخص کن.

    دنبال توابع طولانی، شرط‌های تو در تو، منطق تکراری، نام‌های مبهم و بخش‌هایی بگرد که مسئولیت‌های نامرتبط دارند.

  4. از ساده‌ترین تغییر شروع کن.

    ابتدا نام‌های مبهم را بهتر کن یا یک منطق تکراری کوچک را استخراج کن. لازم نیست از همان ابتدا ساختار تمام پروژه را تغییر بدهی.

  5. یک مسئولیت را جدا کن.

    اگر تابع اصلی چند کار انجام می‌دهد، یکی از مسئولیت‌ها را به تابع مستقلی تبدیل کن. اگر بخش رابط کاربری مستقل است، آن را به کامپوننت جداگانه انتقال بده.

  6. پس از هر مرحله بررسی کن.

    تست‌ها را اجرا کن، نوع‌ها را بررسی کن و رفتار کامپوننت را در مرورگر آزمایش کن.

  7. تغییرات را بازبینی کن.

    با git diff ببین دقیقاً چه چیزی تغییر کرده است. مطمئن شو تغییر ساختاری، ناخواسته رفتار برنامه را عوض نکرده باشد.

  8. تغییرات کوچک را ثبت کن.

    بعد از کامل و بررسی شدن یک بخش، آن را با یک پیام واضح در Git ثبت کن. این کار بازگشت به نسخه قبلی و بررسی تغییرات را آسان‌تر می‌کند.

یک مثال از برنامه روزانه بازآرایی

فرض کن یک کامپوننت ۴۰۰ خطی برای ویرایش مقاله داری. لازم نیست در یک روز تمام ساختار آن را عوض کنی.

  • مرحله اول: شناسایی رفتارها و ثبت تست‌های اصلی.
  • مرحله دوم: بهبود نام متغیرها و استخراج توابع ساده.
  • مرحله سوم: جدا کردن پیش‌نمایش مقاله از فرم ویرایش.
  • مرحله چهارم: انتقال منطق مستقل درخواست‌های API.
  • مرحله پنجم: اجرای تست‌ها و بررسی نهایی.

این مراحل اجباری نیستند و ترتیب مناسب به ساختار واقعی کامپوننت بستگی دارد. ممکن است ابتدا لازم باشد یک باگ را اصلاح کنی یا برای رفتار حساس تست بنویسی.

۱۴. چک‌لیست نهایی برای تمیز کردن یک کامپوننت

قبل از اینکه بگویی کامپوننت را بازآرایی کرده‌ای، این موارد را بررسی کن:

  • نام متغیرها و توابع تا حد مناسبی واضح است.
  • هر تابع مسئولیت مشخصی دارد.
  • شرط‌های پیچیده تا حد امکان خوانا شده‌اند.
  • منطق تکراری واقعی به شکل مناسبی استخراج شده است.
  • کامپوننت‌های مستقل، در صورت نیاز، از هم جدا شده‌اند.
  • Stateهای غیرضروری یا قابل محاسبه حذف شده‌اند.
  • منطق درخواست‌های API با ساختار مناسبی مدیریت می‌شود.
  • نوع داده‌ها و پراپ‌های TypeScript مشخص هستند.
  • قواعد اعتبارسنجی و دسترسی سمت سرور همچنان برقرارند.
  • تست‌ها و بررسی‌های لازم پس از تغییرات موفق بوده‌اند.
  • تغییرات با Git بازبینی شده‌اند.
  • کد جدید از کد قبلی قابل فهم‌تر و نگهداری آن آسان‌تر است.

جمع‌بندی: از کجا شروع کنم؟

اگر امروز یک کامپوننت شلوغ داری، لازم نیست فوراً سراغ معماری پیچیده، الگوهای طراحی یا تقسیم پروژه به ده‌ها فایل بروی.

ابتدا رفتار کامپوننت را بشناس. بعد نام‌گذاری، مسئولیت توابع، شرط‌ها و تکرار کد را بررسی کن. در ادامه، بخش‌هایی را که واقعاً مستقل هستند به توابع یا کامپوننت‌های جدا تبدیل کن.

مهم‌تر از همه، هر بار یک تغییر کوچک انجام بده و نتیجه را بررسی کن. بازآرایی خوب قرار نیست فقط ظاهر کد را زیباتر کند؛ باید تغییر دادن و نگهداری برنامه را هم آسان‌تر کند.

این جمله را به خاطر بسپار: کد تمیز، کدی نیست که فقط امروز کار کند؛ کدی است که تو و برنامه‌نویس بعدی بتوانید فردا هم آن را بفهمید، تغییر دهید و با اطمینان توسعه‌اش دهید.