برنامه نویسی

Refs در React: از دسترسی به DOM تا API ضروری

توضیحات تصویر

در ابتدا در https://www.developerway.com منتشر شد. این وب سایت مقالات بیشتری از این قبیل دارد 😉


یکی از چیزهای زیبای React این است که پیچیدگی برخورد با DOM واقعی را از بین می برد. اکنون، به‌جای جستجوی دستی عناصر، فکر کردن درباره نحوه افزودن کلاس‌ها به آن عناصر، یا مبارزه با ناهماهنگی‌های مرورگر، می‌توانیم فقط مؤلفه‌ها را بنویسیم و روی تجربه کاربر تمرکز کنیم. با این حال، هنوز مواردی وجود دارد (هر چند بسیار اندک!) که باید به DOM واقعی دسترسی داشته باشیم.

و وقتی نوبت به DOM واقعی می‌رسد، مهم‌ترین چیز برای درک و یادگیری نحوه استفاده صحیح Ref و همه چیز اطراف Ref است. بنابراین، امروز، بیایید نگاهی بیندازیم به این که چرا در وهله اول می‌خواهیم به DOM دسترسی داشته باشیم، Ref چگونه می‌تواند به آن کمک کند، چه چیزهایی هستند. useRef، forwardRef و useImperativeHandleو نحوه استفاده صحیح از آنها. همچنین، بیایید بررسی کنیم که چگونه اجتناب کردن استفاده كردن forwardRef و useImperativeHandle در حالی که هنوز آنچه را که به ما می دهند را داریم. اگر تا به حال سعی کرده اید بفهمید که چگونه کار می کنند، خواهید فهمید که چرا ما آن را می خواهیم

و به عنوان یک امتیاز، یاد خواهیم گرفت که چگونه API های ضروری را در React پیاده سازی کنیم!

این مقاله به‌عنوان ویدیوی YouTube نیز موجود است: جزئیات کمتری دارد اما در عوض انیمیشن‌های زیبایی دارد. گاهی اوقات درک یک مفهوم از یک انیمیشن 3 ثانیه ای آسان تر از دو پاراگراف متن است.

فرض کنید می‌خواهم برای کنفرانسی که برگزار می‌کنم، فرم ثبت‌نام را اجرا کنم. من از مردم می خواهم قبل از اینکه بتوانم جزئیات را برای آنها ارسال کنم، نام، ایمیل و دسته توییتر خود را به من بدهند. فیلدهای “نام” و “ایمیل” را می خواهم اجباری کنم. اما نمی‌خواهم برخی از حاشیه‌های قرمز آزاردهنده را در اطراف آن ورودی‌ها نشان دهم، وقتی افراد سعی می‌کنند آنها را خالی ارسال کنند، می‌خواهم فرم جذاب باشد. بنابراین، در عوض، می‌خواهم میدان خالی را متمرکز کنم و آن را کمی تکان دهم تا توجه را جلب کنم، فقط برای سرگرمی.

حالا React به ما چیزهای زیادی می دهد، اما به ما نمی دهد همه چيز. چیزهایی مانند “تمرکز یک عنصر به صورت دستی” بخشی از بسته نیست. برای آن، ما باید مهارت‌های زنگ‌زده بومی جاوا اسکریپت API خود را پاک کنیم. و برای آن، ما نیاز به دسترسی به عنصر DOM واقعی داریم.

در دنیای بدون واکنش، ما کاری شبیه به این انجام می دهیم:

const element = document.getElementById("bla");
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پس از آن می توانیم آن را متمرکز کنیم:

element.focus();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یا به آن بروید:

element.scrollIntoView();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یا هر چیز دیگری که دلمان بخواهد. برخی از موارد استفاده معمول برای استفاده از DOM API بومی در دنیای React عبارتند از:

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

و اگرچه، از نظر فنی، هیچ چیز ما را از انجام آن باز نمی دارد getElementById حتی امروزه، React راه کمی قدرتمندتر برای دسترسی به آن عنصر به ما می دهد که نیازی به انتشار شناسه ها در همه جا یا آگاهی از ساختار DOM اساسی ندارد: refs.

Ref فقط یک شیء قابل تغییر است، مرجعی که React بین رندرهای مجدد به آن حفظ می کند. رندر مجدد را راه اندازی نمی کند، بنابراین به هیچ وجه جایگزینی برای حالت نیست، سعی نکنید از آن برای آن استفاده کنید. جزئیات بیشتر در مورد تفاوت بین این دو در اسناد موجود است.

با ایجاد شده است useRef قلاب:

const Component = () => {
  // create a ref with default value null
  const ref = useRef(null);

  return ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و مقدار ذخیره شده در Ref در ویژگی “جاری” (و تنها) آن موجود خواهد بود. و ما در واقع می توانیم هر چیزی را در آن ذخیره کنیم! برای مثال، می‌توانیم یک شی را با مقادیری از حالت ذخیره کنیم:

const Component = () => {
  const ref = useRef(null);

  useEffect(() => {
    // re-write ref's default value with new object
    ref.current = {
      someFunc: () => {...},
      someValue: stateValue,
    }
  }, [stateValue])


  return ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یا مهمتر از آن برای مورد استفاده ما، می توانیم این Ref را به هر عنصر DOM و برخی از اجزای React اختصاص دهیم:

const Component = () => {
  const ref = useRef(null);

  // assing ref to an input element
  return <input ref={ref} />
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا اگر لاگین کنم ref.current که در useEffect (فقط پس از رندر شدن یک مؤلفه در دسترس است)، خواهم دید دقیقا همان عنصری که اگر بخواهم انجام دهم، به دست می‌آورم getElementById در آن ورودی:

const Component = () => {
  const ref = useRef(null);

  useEffect(() => {
    // this will be a reference to input DOM element!
    // exactly the same as if I did getElementById for it
    console.log(ref.current);
  });

  return <input ref={ref} />
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و اکنون، اگر من فرم ثبت نام خود را به عنوان یک جزء غول پیکر پیاده سازی می کردم، می توانستم کاری شبیه به این انجام دهم:

const Form = () => {
  const [name, setName] = useState('');
  const inputRef = useRef(null);

  const onSubmitClick = () => {
    if (!name) {
      // focus the input field if someone tries to submit empty name
      ref.current.focus();
    } else {
      // submit the data here!
    }
  }

  return <>
    ...
    <input onChange={(e) => setName(e.target.value)} ref={ref} />
    <button onClick={onSubmitClick}>Submit the form!</button>
  </>
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مقادیر ورودی‌ها را در حالت ذخیره کنید، برای همه ورودی‌ها ref ایجاد کنید و وقتی دکمه «submit» کلیک می‌شود، بررسی می‌کنم که آیا مقادیر خالی نیستند و اگر هستند – ورودی مورد نیاز را متمرکز کنید.

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

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

const InputField = ({ onChange, label }) => {
  return <>
    {label}<br />
    <input type="text" onChange={(e) => onChange(e.target.value)} />
  </>
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اما عملکرد رسیدگی به خطا و ارسال آن همچنان در ادامه خواهد بود Form، نه ورودی!

const Form = () => {
  const [name, setName] = useState('');

  const onSubmitClick = () => {
    if (!name) {
      // deal with empty name
    } else {
      // submit the data here!
    }
  }

  return <>
    ...
    <InputField label="name" onChange={setName} />
    <button onClick={onSubmitClick}>Submit the form!</button>
  </>
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چگونه می توانم به ورودی بگویم که از مؤلفه Form “خود را متمرکز کند”؟ روش “عادی” برای کنترل داده ها و رفتار در React این است که به اجزای سازنده و گوش دادن به تماس ها پاسخ دهید. من می توانم سعی کنم پایه “focusItself” را به آن منتقل کنم InputField که از آن سوئیچ کنم false به true، اما این فقط یک بار کار می کند.

// don't do this! just to demonstate how it could work in theory
const InputField = ({ onChange, focusItself }) => {
  const inputRef = useRef(null);

  useEffect(() => {
    if (focusItself) {
      // focus input if the focusItself prop changes
      // will work only once, when false changes to true
      ref.current.focus();
    }
  }, [focusItself])

  // the rest is the same here
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

می‌توانم سعی کنم مقداری پاسخ به تماس «onBlur» اضافه کنم و آن را بازنشانی کنم focusItself پشتیبانی به false هنگامی که ورودی تمرکز خود را از دست می دهد، یا به جای بولی با مقادیر تصادفی بازی می کنید، یا راه حل خلاقانه دیگری ارائه می دهید.

به احتمال زیاد، راه دیگری وجود دارد. به‌جای اینکه سر و سامان‌دهنده‌ها باشیم، می‌توانیم Ref را در یک جزء ایجاد کنیم (Form، آن را به مؤلفه دیگر منتقل کنید (InputFieldو آن را به عنصر DOM زیرین آنجا وصل کنید. Ref فقط یک شیء قابل تغییر است.

Form سپس Ref را به طور معمول ایجاد می کند:

const Form = () => {
  // create the Ref in Form component
  const inputRef = useRef(null);

  ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و InputField کامپوننت دارای پایه ای خواهد بود که ref را می پذیرد و خواهد داشت input فیلدی که طبق معمول رف را می پذیرد. فقط Ref، به جای ایجاد در InputField، از وسایل موجود در آنجا خواهد آمد:

const InputField = ({ inputRef }) => {
  // the rest of the code is the same

  // pass ref from prop to the internal input component
  return <input ref={inputRef} ... />
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Ref است قابل تغییر شی، به این ترتیب طراحی شده است. وقتی آن را به یک عنصر می‌دهیم، React زیر آن فقط آن را جهش می‌دهد. و شیئی که قرار است جهش پیدا کند در قسمت اعلام می شود Form جزء. پس به محض اینکه InputField رندر می شود، شی Ref جهش می یابد و ما Form دسترسی خواهد داشت input عنصر DOM در inputRef.current:

const Form = () => {
  // create the Ref in Form component
  const inputRef = useRef(null);

  useEffect(() => {
    // the "input" element, that is rendered inside InputField, will be here
    console.log(inputRef.current);
  }, []);

  return (
    <>
      {/* Pass ref as prop to the input field component */}
      <InputField inputRef={inputRef} />
    </>
  )
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یا در ارسال callback خود می‌توانیم inputRef.current.focus()، دقیقاً همان کد قبلی را فراخوانی کنیم.

مثال را در اینجا بررسی کنید:

اگر تعجب می‌کنید که چرا من آن را نام بردم inputRef، به جای اینکه فقط ref: در واقع به این سادگی نیست. ref یک تکیه گاه واقعی نیست، به نوعی یک نام “محفوظ” است. در زمان های قدیم، زمانی که ما هنوز در حال نوشتن کامپوننت های کلاس بودیم، اگر ref را به یک جزء کلاس منتقل می کردیم، نمونه این کامپوننت می شد .current ارزش آن Ref.

اما اجزای عملکردی نمونه ندارند. بنابراین، در عوض، ما فقط یک هشدار در کنسول دریافت می‌کنیم: «اجزای عملکرد را نمی‌توان refs داد. تلاش برای دسترسی به این مرجع با شکست مواجه خواهد شد. آیا منظور شما از React.forwardRef() بود؟

const Form = () => {
  const inputRef = useRef(null);

  // if we just do this, we'll get a warning in console
  return <InputField ref={inputRef} />
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای اینکه این کار انجام شود، باید به React سیگنال بدهیم که این است ref در واقع عمدی است، ما می خواهیم با آن چیزهایی انجام دهیم. ما می توانیم آن را با کمک انجام دهیم forwardRef تابع: مؤلفه و ما را می پذیرد تزریق می کند داور از ref ویژگی به عنوان آرگومان دوم تابع مؤلفه. درست بعد از وسایل

// normally, we'd have only props there
// but we wrapped the component's function with forwardRef
// which injects the second argument - ref
// if it's passed to this component by its consumer
const InputField = forwardRef((props, ref) => {
  // the rest of the code is the same

  return <input ref={ref} />
})
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حتی می‌توانیم کد بالا را برای خوانایی بهتر به دو متغیر تقسیم کنیم:

const InputFieldWithRef = (props, ref) => {
  // the rest is the same
}

// this one will be used by the form
export const InputField = forwardRef(InputFieldWithRef);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و حالا Form فقط می توانید رفرنس را به InputField جزء به عنوان یک عنصر DOM معمولی بود:

return <InputField ref={inputRef} />
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آیا شما باید استفاده کنید forwardRef یا فقط پاس کردن ref به عنوان یک پایه فقط یک موضوع سلیقه شخصی است: نتیجه نهایی یکسان است.

مثال زنده را اینجا ببینید:

بسیار خوب، تمرکز بر ورودی از Form جزء مرتب شده است، به نوعی. اما ما به هیچ وجه با فرم خوب خودمان تمام نشده ایم. به خاطر دارید، ما می خواستیم در هنگام بروز خطا علاوه بر تمرکز، ورودی را تکان دهیم؟ چنین چیزی وجود ندارد element.shake() در API جاوا اسکریپت بومی، بنابراین دسترسی به عنصر DOM در اینجا کمکی نخواهد کرد 😢

ما به راحتی می توانیم آن را به عنوان یک انیمیشن CSS پیاده سازی کنیم:

const InputField = () => {
  // store whether we should shake or not in state
  const [shouldShake, setShouldShake] = useState(false);

  // just add the classname when it's time to shake it - css will handle it
  const className = shouldShake ? "shake-animation" : '';

  // when animation is done - transition state back to false, so we can start again if needed
  return <input className={className} onAnimationEnd={() => setShouldShake(false)} />
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اما چگونه آن را تحریک کنیم؟ باز هم، همان داستان قبلی با تمرکز – من می‌توانم با استفاده از لوازم جانبی راه‌حل خلاقانه‌ای پیدا کنم، اما عجیب به نظر می‌رسد و به طور قابل توجهی فرم را بیش از حد پیچیده می‌کند. به خصوص با توجه به اینکه ما فوکوس را از طریق ref کنترل می کنیم، بنابراین دقیقاً برای یک مشکل دو راه حل خواهیم داشت. اگر فقط می توانستم کاری انجام دهم InputField.shake() و InputField.focus() اینجا!

و صحبت از تمرکز – چرا من Form مؤلفه هنوز باید با DOM API بومی برای راه‌اندازی آن مقابله کند؟ آیا این مسئولیت و تمام هدف نیست InputField، برای انتزاع از پیچیدگی ها مانند این؟ چرا فرم حتی به عنصر DOM اساسی دسترسی دارد – اساساً جزئیات پیاده سازی داخلی را فاش می کند. را Form جزء نباید اهمیتی بدهد که از کدام عنصر DOM استفاده می‌کنیم یا اصلاً از عناصر DOM یا چیز دیگری استفاده می‌کنیم. تفکیک نگرانی ها، می دانید.

به نظر می رسد زمان آن رسیده است که یک API ضروری مناسب را برای خود پیاده سازی کنیم InputField جزء. در حال حاضر، React اعلامی است و از همه ما انتظار دارد که کد خود را بر اساس آن بنویسیم. اما گاهی اوقات ما فقط به راهی نیاز داریم تا به طور ضروری چیزی را تحریک کنیم. احتمالاً React یک دریچه فرار برای آن به ما می دهد: useImperativeHandle hook.

درک این قلاب کمی گیج کننده است، من مجبور شدم دو بار اسناد را بخوانم، چند بار آن را امتحان کنم و اجرای آن را در کد واقعی React انجام دهم تا واقعاً به کاری که انجام می دهد پی ببرم. اما اساساً ما فقط به دو چیز نیاز داریم: تصمیم بگیرید که API ضروری ما چگونه باشد و یک Ref برای پیوست کردن آن. برای ورودی ما، ساده است: ما فقط نیاز داریم .focus() و .shake() به عنوان یک API عمل می کند، و ما از قبل همه چیز را در مورد refs می دانیم.

// this is how our API could look like
const InputFieldAPI = {
  focus: () => {
    // do the focus here magic
  },
  shake: () => {
    // trigger shake here
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این useImperativeHandle hook فقط این شی را به ویژگی “current” آبجکت Ref متصل می کند، همین. این کار را به این صورت انجام می دهد:

const InputField = () => {

  useImperativeHandle(someRef, () => ({
    focus: () => {},
    shake: () => {},
  }), [])

}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اولین آرگومان – Ref ما است که یا در خود مؤلفه ایجاد می شود، از props یا از طریق forwardRef منتقل می شود. آرگومان دوم تابعی است که یک شی را برمی گرداند – این شیئی است که به عنوان در دسترس خواهد بود inputRef.current. و سومین آرگومان آرایه وابستگی ها است، مانند هر هوک React دیگری.

برای کامپوننت ما، اجازه دهید ref را به طور صریح به عنوان ارسال کنیم apiRef پشتیبانی و تنها کاری که باقی مانده است پیاده سازی API واقعی است. برای آن ما به یک مرجع دیگر نیاز داریم – این بار داخلی InputField، تا بتوانیم آن را به input عنصر DOM و تمرکز ماشه طبق معمول:

// pass the Ref that we'll use as our imperative API as a prop
const InputField = ({ apiRef }) => {
  // create another ref - internal to Input component
  const inputRef = useRef(null);

  // "merge" our API into the apiRef
  // the returned object will be available for use as apiRef.current
  useImperativeHandle(apiRef, () => ({
    focus: () => {
      // just trigger focus on internal ref that is attached to the DOM object
      inputRef.current.focus()
    },
    shake: () => {},
  }), [])

  return <input ref={inputRef} />
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و برای “تکان” ما فقط به روز رسانی حالت را راه اندازی می کنیم:

// pass the Ref that we'll use as our imperative API as a prop
const InputField = ({ apiRef }) => {
  // remember our state for shaking?
  const [shouldShake, setShouldShake] = useState(false);

  useImperativeHandle(apiRef, () => ({
    focus: () => {},
    shake: () => {
      // trigger state update here
      setShouldShake(true);
    },
  }), [])

  return ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و بوم! ما Form فقط می تواند ایجاد کند ref، آن را به InputField و قادر به انجام ساده خواهد بود inputRef.current.focus() و inputRef.current.shake()، بدون نگرانی از اجرای داخلی آنها!

const Form = () => {
  const inputRef = useRef(null);
  const [name, setName] = useState('');

  const onSubmitClick = () => {
    if (!name) {
      // focus the input if the name is empty
      inputRef.current.focus();
      // and shake it off!
      inputRef.current.shake();
    } else {
      // submit the data here!
    }
  }

  return <>
    ...
    <InputField label="name" onChange={setName} apiRef={inputRef} />
    <button onClick={onSubmitClick}>Submit the form!</button>
  </>
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با مثال فرم کار کامل در اینجا بازی کنید:

اگر useImperativeHandle قلاب همچنان چشم شما را تکان می‌دهد – نگران نباشید، چشم من هم تکان می‌خورد! اما در واقع مجبور نیستیم از آن برای اجرای عملکردی که به تازگی اجرا کردیم استفاده کنیم. ما قبلاً می دانیم که داورها چگونه کار می کنند و این واقعیت که آنها قابل تغییر هستند. بنابراین تنها چیزی که نیاز داریم این است که شیء API خود را به آن اختصاص دهیم ref.current از Ref مورد نیاز، چیزی شبیه به این:

const InputField = ({ apiRef }) => {
  useEffect(() => {
    apiRef.current = {
      focus: () => {},
      shake: () => {},
    }
  }, [apiRef])
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این تقریباً دقیقاً همان چیزی است useImperativeHandle به هر حال زیر کاپوت انجام می دهد. و دقیقا مثل قبل کار خواهد کرد.

در حقیقت، useLayoutEffect ممکن است اینجا حتی بهتر باشد، اما این موضوع برای مقاله دیگری است. فعلا بریم سراغ سنتی useEffect.

نمونه نهایی را اینجا ببینید:


بله، یک فرم باحال با جلوه تکان دادن خوب آماده است، React refs دیگر یک راز نیست، و API ضروری در React در واقع یک چیز است. چقدر باحاله؟

فقط به یاد داشته باشید: Refs یک “دریچه فرار” است، این جایگزینی برای وضعیت یا عادی جریان داده React با props و callbacks نیست. فقط زمانی از آنها استفاده کنید که هیچ جایگزین “عادی” وجود نداشته باشد. همان روش ضروری برای راه اندازی چیزی – احتمال بیشتری وجود دارد که جریان لوازم / تماس های معمولی همان چیزی است که شما می خواهید.

برای تقویت دانش مقاله را در قالب یوتیوب تماشا کنید 😉


در ابتدا در https://www.developerway.com منتشر شد. این وب سایت مقالات بیشتری از این قبیل دارد 😉

مشترک شدن در خبرنامه، اتصال در لینکدین یا در توییتر دنبالش کن به محض انتشار مقاله بعدی مطلع شوید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا