useState

ما هي useState؟

هي Hook من رياكت تسمح لك بإضافة حالة (state) داخل المكوّنات الدالّية (Function Components).
ترجّع مصفوفتين:

  1. القيمة الحالية للحالة
  2. دالة لتحديث الحالة
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0); // 0 هي القيمة الابتدائية
  return (
    <button onClick={() => setCount(count + 1)}>
      عدّاد: {count}
    </button>
  );
}
JSX
التوقيع (Signature)
const [state, setState] = useState<T>(initialState | () => T);
JSX
  • initialState قد تكون قيمة مباشرة أو دالة تُرجع القيمة الابتدائية (تُستدعى مرة واحدة فقط عند أول رندر).
  • setState إمّا تُمرّر لها قيمة جديدة أو دالة تحديث تعتمد على القيمة السابقة.
القواعد الذهبية (Rules)
  • تُستدعى useState فقط في أعلى المكوّن (وليس داخل شروط/حلقات/دوال داخلية).
  • ترتيب الاستدعاءات مهم؛ لا تغيّره بين الرندرات.
  • لا تعدّل الحالة مباشرة (لا تعمل state.push() على المصفوفات…)، بل أنشئ نسخة جديدة.
1) عدّاد بسيط (Basic Counter)
الكود
import { useState } from "react";

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

  const increment = () => setCount(prev => prev + 1);

  return (
    <button onClick={increment}>
      عدّاد: {count}
    </button>
  );
}
export default Counter;
JSX

الشرح

  • useState(0): بننشئ حالة اسمها count وقيمتها الابتدائية 0 + دالة تحديث setCount.
  • increment: استخدمنا Functional Update (prev => prev + 1) لضمان إن القيمة مبنية على آخر state حقيقية حتى لو صار batching.
  • في الـ return: زر يعرض القيمة الحالية، وكل ضغطة تشغّل increment فتزيد القيمة وتعيد الرندر.
2) حالة بدائية (Boolean Toggle)
الكود
import { useState } from "react";

function Toggle() {
  const [on, setOn] = useState(false);

  const toggle = () => setOn(prev => !prev);

  return (
    <button onClick={toggle}>
      الحالة: {on ? "ON" : "OFF"}
    </button>
  );
}
export default Toggle;
JSX
الشرح
  • الحالة من نوع Boolean تبدأ بـ false.
  • دالة toggle تقلب القيمة بالاعتماد على السابقة.
  • الواجهة تعكس النتيجة نصيًا.
3) حالة كائن (Object State) وتحديث حقول
الكود
import { useState } from "react";

function UserCard() {
  const [user, setUser] = useState({ name: "", age: 0 });

  const updateName = (e) => {
    setUser(prev => ({ ...prev, name: e.target.value }));
  };

  const incrementAge = () => {
    setUser(prev => ({ ...prev, age: prev.age + 1 }));
  };

  return (
    <div>
      <input placeholder="الاسم" value={user.name} onChange={updateName} />
      <button onClick={incrementAge}>+ العمر</button>
      <p>الاسم: {user.name} — العمر: {user.age}</p>
    </div>
  );
}
export default UserCard;
JSX
الشرح
  • مهم: لا نعدل الكائن مباشرة. نأخذ نسخة بـ ...prev ونغيّر الحقل المطلوب.
  • updateName: يأخذ النص من الحقل ويحدث name.
  • incrementAge: يزيد age بدون لمس بقية الحقول.
4) حالة مصفوفة (Array) — مهام: إضافة/حذف/تبديل
الكود
import { useState } from "react";

function Todos() {
  const [text, setText] = useState("");
  const [todos, setTodos] = useState([]);

  const addTodo = () => {
    if (!text.trim()) return;
    setTodos(prev => [
      ...prev,
      { id: crypto.randomUUID(), text: text.trim(), done: false },
    ]);
    setText("");
  };

  const toggleTodo = (id) => {
    setTodos(prev =>
      prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  };

  const removeTodo = (id) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  };

  return (
    <div>
      <input
        placeholder="أضف مهمة…"
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && addTodo()}
      />
      <button onClick={addTodo}>إضافة</button>

      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <label>
              <input
                type="checkbox"
                checked={t.done}
                onChange={() => toggleTodo(t.id)}
              />
              <span style={{ textDecoration: t.done ? "line-through" : "none" }}>
                {t.text}
              </span>
            </label>
            <button onClick={() => removeTodo(t.id)}>حذف</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
export default Todos;
JSX
الشرح
  • إضافة: ننشئ عنصر جديد ونُلحقه بنسخة جديدة من المصفوفة (...prev).
  • تبديل: نستخدم map لإنشاء نسخة جديدة؛ لو الـ id مطابق نقلب done.
  • حذف: نستخدم filter لاستبعاد العنصر المطلوب.
5) التهيئة الكسولة (Lazy Initialization)
الكود
import { useState } from "react";

function heavyCompute() {
  // محاكاة حساب ثقيل
  let s = 0;
  for (let i = 0; i < 2_000_000; i++) s += i;
  return s;
}

function LazyInit() {
  const [value, setValue] = useState(() => heavyCompute());

  return (
    <div>
      <p>قيمة مبدئية محسوبة: {value}</p>
      <button onClick={() => setValue(v => v + 1)}>+1</button>
    </div>
  );
}
export default LazyInit;
JSX
الشرح
  • مررنا دالة لـ useState بدل قيمة مباشرة، فتُستدعى مرة واحدة فقط عند أول render.
  • مناسب لما تكون الحسابات الابتدائية ثقيلة.
6) التحديثات المتتالية المضمونة (Functional Updates)
الكود
import { useState } from "react";

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

  const addTwo = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return (
    <div>
      <p>القيمة: {count}</p>
      <button onClick={addTwo}>زوّد 2</button>
    </div>
  );
}
export default DoubleIncrement;
JSX
الشرح
  • استدعينا setCount مرتين بصيغة تعتمد على prev.
  • React قد يعمل batching، لكن بهذه الصيغة النتيجة مضمونة: +2.
7) إدارة نموذج (Form) بسيط
الكود
import { useState } from "react";

function LoginForm() {
  const [form, setForm] = useState({ email: "", password: "" });

  const onChange = (e) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  const onSubmit = (e) => {
    e.preventDefault();
    console.log("Submitting:", form);
  };

  return (
    <form onSubmit={onSubmit}>
      <input name="email" placeholder="Email" value={form.email} onChange={onChange} />
      <input name="password" type="password" placeholder="Password" value={form.password} onChange={onChange} />
      <button type="submit">دخول</button>
    </form>
  );
}
export default LoginForm;
JSX
الشرح
  • حالة الكائن تحفظ حقول النموذج.
  • كل تغيير يحدّث الحقل المناسب عبر name.
  • عند الإرسال نمنع تحديث الصفحة (preventDefault) ونتعامل مع البيانات.
8) مؤقّت بسيط باستخدام useEffect (Stopwatch)
الكود
import { useEffect, useState } from "react";

function Stopwatch() {
  const [sec, setSec] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setSec(prev => prev + 1), 1000);
    return () => clearInterval(id); // تنظيف عند التفكيك
  }, []);

  return <div>{sec}s</div>;
}
export default Stopwatch;
JSX
الشرح
  • useEffect بدون تبعيات (مصفوفة فارغة) ⇒ يُشغَّل مرة بعد الرندر الأول.
  • ننشئ setInterval يحدّث الحالة كل ثانية.
  • تنظيف بـ clearInterval عند إزالة المكوّن لمنع تسريب مؤقتات.
9) تبديل الثيم (Theme Toggle)
الكود
import { useState } from "react";

function ThemeSwitcher() {
  const [theme, setTheme] = useState("light");
  const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));

  return (
    <button onClick={toggle}>
      الثيم الحالي: {theme}
    </button>
  );
}
export default ThemeSwitcher;
JSX
الشرح
  • نخزّن نص الثيم في state.
  • نقلب بين “light/dark” بدالة تعتمد على القيمة الحالية.
  • يمكنك لاحقًا ربطها بـ <html data-theme={theme}> أو كلاس CSS.
10) عدّاد بخطوة ديناميكية (Prop)
الكود
import { useState } from "react";

function FlexibleCounter({ step = 1 }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c - step)}>-{step}</button>
      <span style={{ margin: "0 8px" }}>{count}</span>
      <button onClick={() => setCount(c => c + step)}>+{step}</button>
    </div>
  );
}
export default FlexibleCounter;
JSX
الشرح
  • نأخذ step من props (بقيمة افتراضية 1).
  • كل زر يضيف/ينقص بالقيمة المعطاة دون تكرار منطق.
11) بحث وتصفية منتجات (Filter + Search)
الكود
import { useState } from "react";

function Products({ items }) {
  const [query, setQuery] = useState("");
  const [onlyInStock, setOnlyInStock] = useState(false);

  const visible = items
    .filter(p => p.name.toLowerCase().includes(query.toLowerCase()))
    .filter(p => (onlyInStock ? p.inStock : true));

  return (
    <>
      <input
        placeholder="ابحث…"
        value={query}
        onChange={e => setQuery(e.target.value)}
      />
      <label style={{ marginLeft: 8 }}>
        <input
          type="checkbox"
          checked={onlyInStock}
          onChange={e => setOnlyInStock(e.target.checked)}
        />
        فقط المتوفر
      </label>

      <ul>
        {visible.map(p => (
          <li key={p.id}>
            {p.name} {p.inStock ? "" : "(غير متوفر)"}
          </li>
        ))}
      </ul>
    </>
  );
}
export default Products;
JSX
الشرح
  • حالتان: نص البحث + فلتر التوفر.
  • نشتق قائمة visible من items (لا نخزنها في state لأنها مشتقة).
  • أي تغيير يعيد الرندر ويطبق الفلاتر مباشرة.
12) أخطاء شائعة + التصحيح
الخطأ 1: تعديل الحالة مباشرة
// ❌ خطأ
user.name = "Ali";
setUser(user);

// ✅ صحيح
setUser(prev => ({ ...prev, name: "Ali" }));
JSX
الخطأ 2: استخدام قيمة قديمة في تحديثات متتالية
// ❌ خطأ
setCount(count + 1);
setCount(count + 1);

// ✅ صحيح
setCount(prev => prev + 1);
setCount(prev => prev + 1);
JSX
الخطأ 3: استدعاء useState داخل شرط/حلقة
  • لازم تكون في أعلى المكوّن دائمًا، بنفس الترتيب بكل رندر.