فهم React Hooks في مشروع Three.js حقيقي (دليل عملي)

عند بناء تطبيقات ثلاثية الأبعاد تفاعلية باستخدام React وThree.js، تعمل خطافات React كجسر بين منطق الواجهة ومحرك العرض. يشرح هذا المقال الخطافات على ثلاث طبقات:

  1. المفهوم العام لخطافات React
  2. الاستخدام الحقيقي داخل مشروع Three.js، خطوة بخطوة
  3. أي الخطافات هي الأهم في التطبيقات الفعلية

🧠 1. نظرة سريعة على خطافات React

تساعد الخطافات المكونات الوظيفية على إدارة:

  • 🟢 الحالة — البيانات التي تغيّر الواجهة
  • 🔵 الآثار الجانبية — العمليات الخارجية
  • 🟣 الحفظ المؤقت (Memoization) — تحسين الأداء

🟢 useState ← حالة الواجهة. للقيم التي تتغير وتؤثر في العرض: الكائن المحدد، اللون المحدد، عناصر التحكم بالحركة.

🔵 useEffect ← الآثار الجانبية. للعمليات خارج عرض React: تهيئة مشهد Three.js، ربط الأحداث، ومزامنة React ← Three.js.

🟣 useRef ← تخزين دائم. لتخزين عناصر DOM، ومشهد Three.js والكاميرا وأداة العرض، ومراجع الشبكات — دون التسبب في إعادة العرض.

🟡 useMemo ← تحسين الأداء. يتجنب الحسابات المكلفة المتكررة ويحفظ الهندسة أو القيم المشتقة.

🔴 useCallback ← دوال مستقرة. يمنع إعادة إنشاء الدوال؛ مفيد لمستمعي الأحداث مثل resize وclick.

🎮 2. خطافات React في مشروع Three.js حقيقي

لنطبّق كل شيء الآن على نظام ثلاثي الأبعاد حقيقي، خطوة بخطوة.

الخطوة 1: useRef ← طبقة محرك Three.js

نخزّن كائنات Three.js الأساسية خارج دورة عرض React:

const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const cubesRef = useRef<THREE.Mesh[]>([]);

لماذا؟ لأن هذه الكائنات ليست حالة للواجهة، ولا يجب أن تسبب إعادة عرض، ويجب أن تبقى بين عمليات العرض.

الخطوة 2: useState ← طبقة الواجهة

const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [selectedColor, setSelectedColor] = useState("#ff9900");
const [moveX, setMoveX] = useState(0);

تحدّث هذه القيم الواجهة، وتتحكم بالتفاعل، وتطلق الآثار.

الخطوة 3: useMemo ← تحسين الهندسة

ثابتة (تُنشأ مرة واحدة) — الهندسة مكلفة، لذا تُنشأ مرة واحدة فقط:

const baseGeometry = useMemo(() => {
  return new THREE.BoxGeometry(1, 1, 1);
}, []);

ديناميكية (تعتمد على الحالة) — قيمة مشتقة من الحالة:

const dynamicOffset = useMemo(() => {
  return moveX * 0.5;
}, [moveX]);

الخطوة 4: useCallback ← دوال مستقرة

معالج تغيير الحجم (دون اعتماديات):

const handleResize = useCallback(() => {
  const camera = cameraRef.current;
  const renderer = rendererRef.current;
  if (!camera || !renderer || !mountRef.current) return;

  camera.aspect = mountRef.current.clientWidth / mountRef.current.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight);
}, []);

معالج النقر (يعتمد على الحالة) — سلوك النقر يعتمد على اللون الحالي:

const handleCubeClick = useCallback(
  (mesh: THREE.Mesh) => {
    const mat = mesh.material as THREE.MeshStandardMaterial;
    mat.color.set(selectedColor);
    setSelectedProduct(mesh.userData as Product);
  },
  [selectedColor]
);

الخطوة 5: useEffect ← تهيئة المحرك

useEffect(() => {
  if (!mountRef.current) return;

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(/* ... */);
  const renderer = new THREE.WebGLRenderer();

  mountRef.current.appendChild(renderer.domElement);
}, []);

يعمل مرة واحدة فقط ويهيّئ المحرك ثلاثي الأبعاد كاملًا: المشهد والكاميرا وأداة العرض والإضاءة والمكعبات.

الخطوة 6: نظام التفاعل (Raycaster)

const onMouseClick = (event: MouseEvent) => {
  raycaster.setFromCamera(mouse, camera);
  const hits = raycaster.intersectObjects(cubesRef.current);

  if (hits.length > 0) {
    handleCubeClick(hits[0].object as THREE.Mesh);
  }
};

يكتشف المكعب الذي نُقر عليه بتحويل موضع الفأرة الثنائي الأبعاد إلى شعاع ثلاثي الأبعاد.

الخطوة 7: حلقة الحركة

const animate = () => {
  requestAnimationFrame(animate);

  cubesRef.current.forEach((cube) => {
    cube.rotation.y += 0.01;
    cube.position.x += dynamicOffset * 0.001;
  });

  renderer.render(scene, camera);
};

تعمل في كل إطار، وتحدّث العالم ثلاثي الأبعاد، وتعرض المشهد باستمرار.

الخطوة 8: التحديثات التفاعلية (useEffect)

مزامنة اللون:

useEffect(() => {
  if (selectedMeshRef.current) {
    (selectedMeshRef.current.material as THREE.MeshStandardMaterial).color.set(selectedColor);
  }
}, [selectedColor]);

تحديث الموضع:

useEffect(() => {
  cubesRef.current.forEach((cube, i) => {
    cube.position.x = i * 2 - 4 + moveX;
  });
}, [moveX]);

🚀 3. ما الذي يعلّمه هذا المشروع

  • React = متحكم الواجهة
  • Three.js = محرك العرض
  • الخطافات = طبقة الجسر

⚡ 4. أي الخطافات أهم في المشاريع الحقيقية؟

🔥 الأهم: useRef (نواة المحرك، حاسم)، وuseEffect (إدارة دورة الحياة، حاسم)، وuseState (التحكم بالواجهة، مهم).

🟡 ثانوي: useMemo (تحسين الأداء) وuseCallback (استقرار الأحداث).

📌 الفكرة الختامية

في معمارية React + Three.js، يدير React الحالة، ويتولى Three.js العرض، وتربط الخطافات العالمين بكفاءة.

الكود الكامل: react-learning-journey على GitHub — راجع المكون Canvas3D.tsx.