Ref ها در کامپوننت‌های کاربردی + معرفی مشکل Prop Chain

Refs in Functional Components

23 بهمن 1399
Ref ها در کامپوننت های کاربردی + معرفی مشکل Prop Chain

استفاده از Ref ها به همراه React Hooks

در قسمت قبل با Ref در React آشنا شدیم و از آن ها در فایل Person.js استفاده کردیم. Person.js یک کامپوننت کلاس-محور است و ما هیچ حرفی از کامپوننت های کاربردی نزدیم بنابراین در این قسمت باید به سراغ کامپوننت های کاربردی و Ref در آن ها برویم. همچنین در جلسه قبل دو نوع روش برای کار با Ref در React به شما معرفی کردیم:

  • استفاده از یک تابع ساده (روش قدیمی)
  • استفاده از constructor (روش جدید)

اگر با کامپوننت های کاربردی سروکار دارید، نمی توانید از روش قدیمی برای کار با Ref استفاده کنید اما از روش جدید (با اعمال تغییرات) می توان استفاده کرد. برای شروع به فایل Cockpit.js بروید:

    return (
        <div className={classes.Cockpit}>
            <h1>{props.title}</h1>
            <p className={assignedClasses.join(' ')}>This is really working!</p>
            <button
                className={btnClass}
                onClick={props.clicked}>Toggle Persons
            </button>
        </div>
    );

فرض کنید ما می خواهیم با هربار بارگذاری صفحه دکمه موجود (button) به صورت خودکار کلیک شود. این مثال فقط جهت یادگیری است، شما باید در پروژه هایتان بر اساس نیاز واقعی از موارد ذکر شده استفاده کنید.

از آنجایی که این کامپوننت از نوع کاربردی (functional) است، هیچ constructor ای وجود ندارد، بنابراین reference خود را درون این تابع می نویسیم:

const toggleBtnRef = React.createRef();

این کد در کامپوننت های کلاس-محور بدون مشکل کار می کند اما در کامپوننت های کاربردی کاملا غلط است. برای ساخت یک Ref در این نوع کامپوننت ها باید از یک Hook به نام useRef استفاده کنید:

import React, { useEffect, useRef } from 'react';

سپس:

const cockpit = (props) => {

    const toggleBtnRef = useRef(null);

	// .... بقیه کدها ....//

ما به عنوان مقدار اولیه null را به آن داده ایم تا بعدا تعیین کنیم که به کدام عنصر اشاره خواهیم کرد. حالا به قسمت JSX میرویم و ref را به button اضافه می کنیم:

    return (
        <div className={classes.Cockpit}>
            <h1>{props.title}</h1>
            <p className={assignedClasses.join(' ')}>This is really working!</p>
            <button
                ref={toggleBtnRef}
                className={btnClass}
                onClick={props.clicked}>Toggle Persons
            </button>
        </div>
    );

همانطور که می بینید ref خود را به دکمه اضافه کرده ایم. حالا باید آن را کلیک کنیم:

const cockpit = (props) => {

    const toggleBtnRef = useRef(null);
    toggleBtnRef.current.click();

	// .... بقیه کدها .... //

اگر الان به مرورگر برویم با خطای زیر روبرو می شویم:

Cannot read property 'click' of null

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

مشکل ما همین قسمت است:

const cockpit = (props) => {

    const toggleBtnRef = useRef(null);
    toggleBtnRef.current.click();

	// .... بقیه کدها .... //

ما در اینجا یک ثابت به نام toggleBtnRef ساخته ایم و سریعا در خط بعدی آن را کلیک کرده ایم! برنامه هنوز به کدهای JSX نرسیده تا بخواهد این Ref را روی دکمه ما تنظیم کنید. کدهای JSX در قسمت انتهایی قرار دارند و از آنجا که هنوز کد زیر اجرا نشده، مقدار toggleBtnRef نیز null است:

<button
      ref={toggleBtnRef}
      className={btnClass}
      onClick={props.clicked}>Toggle Persons
</button>

راه حل مشکل چیست؟ راه حل استفاده از همین useEffect است چرا که ما یاد گرفتیم useEffect پس از پایان هر چرخه render اجرا می شود. بنابراین اگر کد خود را درون آن بنویسیم به جای آنکه در لحظه اجرا شود، پس از render شدن کدهای JSX اجرا خواهد شد. در حال حاضر useEffect زیر را برای نمایش پیام همیشگی خود داریم:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);

        return () => {
            console.log('[Cockpit.js] cleanup work in useEffect');
        };

    }, []);

اگر یادتان باشد به عنوان آرگومان دوم به آن یک تابع خالی دادیم تا فقط یک بار اجرا شود. حالا دیگر نیازی به این تایمر و نمایش پیام alert نداریم بنابراین آن را کامنت کرده و کد خودمان را اضافه می کنیم:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        // setTimeout(() => {
        //     alert('saved data to cloud!');
        // }, 1000);
        toggleBtnRef.current.click();

        return () => {
            console.log('[Cockpit.js] cleanup work in useEffect');
        };

    }, []);

حالا اگر به مرورگر بروید برنامه را بدون هیچ نقصی مشاهده خواهید کرد. برنامه به محض render شدن صفحه، دکمه Toggle Persons را کلیک می کند.

مشکل معروف Prop Chain (زنجیره های prop)

react قابلیت بسیار خوبی دارد که با کمک آن می توانید از زنجیره های بسیار طولانی prop رهایی پیدا کنید. بگذارید برایتان توضیح بدهم، به فایل Person.js بروید. فرض کنید در برنامه خود قسمتی برای احراز هویت (authentication) داریم و می خواهیم نتیجه عملیات احراز هویت را (به صورت یک پیام) در قسمت JSX کامپوننت Person.js به کاربر نمایش دهیم. خود عملیات احراز هویت را در cockpit.js انجام می دهیم بنابراین این فایل را باز کنید:

    return (
        <div className={classes.Cockpit}>
            <h1>{props.title}</h1>
            <p className={assignedClasses.join(' ')}>This is really working!</p>
            <button
                ref={toggleBtnRef}
                className={btnClass}
                onClick={props.clicked}>Toggle Persons
            </button>
            <button onClick={props.login}>Log in</button>
        </div>
    );

من به این فایل یک دکمه جدید log in به همراه onClick اضافه کرده ام که قرار است یک prop به نام login دریافت کند (شما می توانید هر نام دیگری را انتخاب کنید). حالا به فایل App.js میرویم و در قسمت JSX یک prop به نام login اضافه می کنیم تا به Cockpit پاس داده شود:

    return (
      <Aux>
        <button
          onClick={() => { this.setState({ showCockpit: false }) }}
        >Remove Cockpit</button>
        {this.state.showCockpit ? (
          <Cockpit
            title={this.props.appTitle}
            showPersons={this.state.showPersons}
            personsLength={this.state.persons.length}
            clicked={this.togglePersonsHandler}
            login={}
            />
        ) : null}
        {persons}
      </Aux>
    );

من prop جدیدی به نام login ایجاد کرده ام اما هنوز چیزی داخل آن ننوشته ام. بیایید در قسمت میانی App.js (پایین تر از togglePersonsHandler) یک متد به نام loginHandler تعریف کنیم:

  loginHandler = () => {
    
  }

برای این مثال فعلا فقط می خواهم یک state به نام authenticated را روی true تنظیم کنم. فعلا نمی خواهیم یک سیستم کامل و واقعی احراز هویت پیاده کنیم. بنابراین به State بروید و آن را اضافه کنید:

  state = {
    persons: [
      { id: 'asfa1', name: 'Max', age: 28 },
      { id: 'vasdf1', name: 'Manu', age: 29 },
      { id: 'asdf11', name: 'Stephanie', age: 26 }
    ],
    otherState: 'some other value',
    showPersons: false,
    showCockpit: true,
    changeCounter: 0,
    authenticated: false
  }

حالا می توانیم loginHandler را کامل کنیم:

  loginHandler = () => {
    this.setState({authenticated: true});
  }

سپس loginHandler را به عنوان prop پاس می دهیم:

    return (
      <Aux>
        <button
          onClick={() => { this.setState({ showCockpit: false }) }}
        >Remove Cockpit</button>
        {this.state.showCockpit ? (
          <Cockpit
            title={this.props.appTitle}
            showPersons={this.state.showPersons}
            personsLength={this.state.persons.length}
            clicked={this.togglePersonsHandler}
            login={this.loginHandler}
            />
        ) : null}
        {persons}
      </Aux>
    );

مشکل بعدی اینجاست که می خواهیم این قابلیت login را برای تک تک افراد (Person) قرار دهیم اما درون App.js فقط به تمام افراد (کامپوننت Persons) دسترسی داریم، بنابراین باید با یک prop اطلاعات را به Person بفرستیم:

    if (this.state.showPersons) {
      persons = <Persons
        persons={this.state.persons}
        clicked={this.deletePersonHandler}
        changed={this.nameChangedHandler}
        isAuthenticated={this.state.authenticated}
        />
    }

من نام این prop را isAuthenticated قرار داده ام اما شما می توانید هر نام دیگری را انتخاب کنید. حالا می توانیم وارد فایل Persons.js شویم و با یک prop دیگر به نام isAuth یا هر نام دیگری آن را به Person پاس دهیم:

            return (
                <Person
                    click={() => this.props.clicked(index)}
                    name={person.name}
                    age={person.age}
                    key={person.id}
                    changed={(event) => this.props.changed(event, person.id)}
                    isAuth={this.props. isAuthenticated }
                    />
            );

بالاخره به فایل Person.js می رسیم و در آن می گوییم:

        return (
            <Aux>
                {this.props.isAuth ? <p>Authenticated!</p> : <p>Please log in</p>}
                <p key="i1" onClick={this.props.click}>I'm {this.props.name} and I am {this.props.age} years old!</p>
                <p key="i2">{this.props.children}</p>
                <input
                    key="i3"
                    // ref={(inputEl) => { this.inputElement = inputEl }}
                    ref={this.inputElementRef}
                    type="text"
                    onChange={this.props.changed} value={this.props.name}
                />
            </Aux>
        );

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

مطمئن هستم که حالا متوجه مشکل prop chain یا زنجیره های طولانی prop شده اید. راه حل این مشکل را در جلسه بعد ارائه خواهیم کرد.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

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