العودة إلى المدونة

GSAP في React وNext.js: ‏useGSAP والتنظيف وScrollTrigger

GSAP في React وNext.js: ‏useGSAP والتنظيف وScrollTrigger

5 أكتوبر 2026 — الحركة

تاريخ النشر

5 أكتوبر 2026

مدة القراءة

3 دقيقة للقراءة

التصنيف

الحركة

الوسوم

  • GSAP
  • React
  • Next.js
  • ScrollTrigger
  • Animation

(مقدمة)كيف تستخدم GSAP في React وفي App Router الخاص بـ Next.js دون تسرّب في الذاكرة أو حركات مكررة. ‏useGSAP، ونطاق المحددات، وcontextSafe، وScrollTrigger، وتغيير الصفحات.

ما زالت GSAP أقوى مكتبة للحركة على الويب، ومنذ الإصدار 3.13 أصبحت كل إضافاتها مجانية: ScrollTrigger وSplitText وMorphSVG وغيرها. لكن React يرسم ويعيد الرسم ويزيل المكونات، ويشغّل التأثيرات مرتين في وضع التطوير، ومكتبة حركة تعدّل DOM مباشرةً تحتاج إلى بعض العناية لتنسجم معه.

هذا هو الإعداد الذي أستخدمه في مشاريع Next.js App Router، ومنها هذا الموقع.

التثبيت

bash
npm install gsap @gsap/react

توفّر @gsap/react الخطّاف useGSAP، الذي يتولى الجزء الذي يخطئ فيه الناس عادةً: التنظيف.

النمط الأساسي

components/hero.tsx
'use client'

import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'

gsap.registerPlugin(useGSAP)

export function Hero() {
  const container = useRef<HTMLDivElement>(null)

  useGSAP(
    () => {
      gsap.from('.hero-line', { yPercent: 100, opacity: 0, stagger: 0.08, duration: 0.9, ease: 'power3.out' })
    },
    { scope: container },
  )

  return (
    <div ref={container}>
      <h1 className="hero-line">Design</h1>
      <h1 className="hero-line">and build</h1>
    </div>
  )
}

تحدث هنا ثلاثة أمور:

  1. 'use client'. تتعامل GSAP مع DOM، لذا يجب أن يعمل المكوّن في المتصفح. اجعل الجزء المتحرك صغيرًا، واترك بقية الصفحة مكونات خادم (Server Components).
  2. scope: container. المحددات مثل .hero-line لا تطابق إلا داخل هذا المكوّن، فلا تحرّك نسختان عناصر بعضهما أبدًا.
  3. التنظيف التلقائي. كل حركة (tween) وخط زمني وScrollTrigger يُنشأ داخل الدالة يُلغى عند إزالة المكوّن. ولهذا أيضًا لا يخلّف التشغيل المزدوج في وضع التطوير حركات مكررة.

إعادة التشغيل عند التغيير

مرّر dependencies لإعادة التشغيل عند تغيّر البيانات، وrevertOnUpdate إن كان يجب التراجع عن التشغيل السابق أولًا:

tsx
useGSAP(
  () => {
    gsap.to('.bar', { scaleX: progress, transformOrigin: 'left', duration: 0.4 })
  },
  { scope: container, dependencies: [progress] },
)

معالجات الأحداث: contextSafe

الحركات التي تُنشأ بعد تشغيل الخطّاف، في معالج نقرة مثلًا، لا تُسجَّل للتنظيف تلقائيًا. غلّف تلك الدوال بـ contextSafe:

tsx
const { contextSafe } = useGSAP({ scope: container })

const onEnter = contextSafe((event: React.MouseEvent<HTMLElement>) => {
  gsap.to(event.currentTarget, { scale: 1.04, duration: 0.3 })
})

return <button onMouseEnter={onEnter}>Hover me</button>

ScrollTrigger

سجّلها مرة واحدة (يكفي استدعاء على مستوى الوحدة)، وأنشئ المشغّلات داخل useGSAP:

components/reveal-section.tsx
'use client'

import { useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useGSAP } from '@gsap/react'

gsap.registerPlugin(useGSAP, ScrollTrigger)

export function RevealSection({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLElement>(null)

  useGSAP(
    () => {
      gsap.utils.toArray<HTMLElement>('.reveal').forEach((el) => {
        gsap.from(el, {
          y: 40,
          opacity: 0,
          duration: 0.8,
          ease: 'power2.out',
          scrollTrigger: { trigger: el, start: 'top 85%' },
        })
      })
    },
    { scope: ref },
  )

  return <section ref={ref}>{children}</section>
}

التثبيت والربط بالتمرير

tsx
useGSAP(
  () => {
    gsap.timeline({
      scrollTrigger: {
        trigger: '.story',
        start: 'top top',
        end: '+=200%',
        scrub: 1,      // follows the scrollbar, with 1 s of smoothing
        pin: true,     // holds the section while the timeline plays
      },
    })
      .to('.story-image', { scale: 1.2 })
      .to('.story-text', { yPercent: -50 }, '<')
  },
  { scope: ref },
)

التحديث بعد تغيّر التخطيط

تقيس ScrollTrigger المواضع عند إنشائها. فإذا غيّرت الصور أو الخطوط أو المحتوى المتأخر ارتفاع الصفحة بعد ذلك، تنحرف مواضع البداية والنهاية. استدعِ ScrollTrigger.refresh() بعد تحميلها:

ts
document.fonts.ready.then(() => ScrollTrigger.refresh())

وإعطاء الصور أبعادًا صريحة (أو aspect-ratio) يمنع معظم هذا من الأساس.

مشكلات خاصة بـ Next.js

تغيير الصفحات وموضع التمرير

في App Router، يؤدي التنقل بين الصفحات إلى إزالة مكونات الصفحة القديمة (فينظّف useGSAP مشغّلاتها) وتركيب مكونات الصفحة الجديدة. إن رأيت مشغّلات تنطلق في مواضع خاطئة بعد التنقل، فالسبب عادةً أن الصفحة الجديدة قاست المواضع قبل تحميل صورها؛ حدّثها بعد التحميل كما في الأعلى.

مكونات الخادم لا تتحرك

لا يمكنك وضع useGSAP في Server Component. وبدلًا من تحويل الصفحة كلها إلى Client Component، غلّف الجزء المتحرك فقط بغلاف صغير يعمل في المتصفح، ومرّر إليه المحتوى المعروض على الخادم بوصفه children. يبقى المحتوى في HTML (وهذا جيد لتحسين محركات البحث وLCP)، وتُضاف الحركة فوقه.

تجنّب وميض المحتوى قبل تحريكه

يعمل gsap.from() بعد الإماهة (hydration)، فيظهر العنصر لإطار واحد في حالته النهائية ثم يقفز إلى حالة البداية. هناك حلّان:

  • استخدم gsap.fromTo() مع CSS يضبط حالة البداية (opacity: 0 في ملف الأنماط)، واقبل أن ظهور العنصر يعتمد على JavaScript؛ وهذا للعناصر غير الأساسية فقط.
  • والأفضل لكل ما يظهر في أعلى الصفحة: الحركة من حالة مرئية (إزاحة صغيرة في y دون تغيير الشفافية)، ليكون الرسم الأول صحيحًا من البداية. وسيشكرك LCP على ذلك؛ راجع مؤشرات أداء الويب للمواقع المتحركة.

إمكانية الوصول: matchMedia

تتيح لك gsap.matchMedia() إنشاء الحركات فقط عندما يتحقق استعلام وسائط، وتلغيها تلقائيًا عندما يتوقف تحققه:

tsx
useGSAP(
  () => {
    const mm = gsap.matchMedia()
    mm.add('(prefers-reduced-motion: no-preference)', () => {
      gsap.from('.card', { y: 60, opacity: 0, stagger: 0.1, scrollTrigger: '.cards' })
    })
  },
  { scope: container },
)

يحصل الزوار الذين يفضّلون تقليل الحركة على المحتوى دون الحركة. والواجهة نفسها تتعامل مع التأثيرات الخاصة بالحاسوب فقط ('(min-width: 768px)').

نصائح للأداء

  • حرّك x وy وscale وrotation وopacity؛ إذ تحوّلها GSAP إلى transforms.
  • استخدم gsap.quickTo() للقيم التي تتحدث باستمرار، مثل عنصر يتبع المؤشر؛ فهي تعيد استخدام حركة واحدة بدل إنشاء حركة جديدة مع كل حدث.
  • أبقِ عدد مشغّلات ScrollTrigger المتزامنة معقولًا، واجمع العناصر المتشابهة بـ ScrollTrigger.batch().

الخلاصة

  • useGSAP مع scope لكل مكوّن متحرك
  • contextSafe للحركات المنشأة في معالجات الأحداث
  • سجّل الإضافات مرة واحدة، وأنشئ مشغّلات ScrollTrigger داخل useGSAP
  • حدّث ScrollTrigger بعد تحميل الخطوط والصور
  • أبقِ المحتوى في مكونات الخادم وحرّكه عبر أغلفة تعمل في المتصفح
  • احترم prefers-reduced-motion باستخدام gsap.matchMedia()

النشرة البريدية

تصلك المقالات الجديدة على بريدك

رسالة قصيرة عند نشر كل جديد. بلا رسائل مزعجة، ويمكنك إلغاء الاشتراك في أي وقت.

أنظمة التصميم للفرق الصغيرة: ابدأ بالرموز
أنظمة التصميم للفرق الصغيرة: ابدأ بالرموز
المقال التالي