کار با key و لیست‌های منعطف (پایان فصل 2) + تمرینات

Working with Keys and Flexible Lists

کار با key و لیست های منعطف (پایان فصل 2) + تمرینات

در جلسه قبل کدهایی نوشتیم که بتواند افراد را حذف کند اما در console خطایی دریافت می کردیم. این خطا به ما می گوید React به دنبال یک prop خاص به نام key می گردد اما آن را پیدا نمی کند. اگر بخواهم به طور خلاصه توضیح دهم می گویم:

Key یک property خاص است که هنگام کار با لیست ها باید نوشته شود. React انسان نیست بنابراین نمی تواند ببیند شما کدام فرد از افراد (لیست) را حذف کرده اید. کد ما در حال حاضر کار می کند اما مسئله اینجاست که react چیزی به نام virtual DOM دارد. در این virtual DOM نسخه ای از DOM آینده (که با متد render نمایش داده می شود) را نگه می دارد تا با DOM فعلی مقایسه کند و هر قسمتی از DOM جدید که با قبلی متفاوت باشد، آن قسمت دوباره render می شود. مشکل اینجاست که در لیست ها باید چیزی مثل یک id وجود داشته باشد که به React اجازه دهد هر کدام افراد را به صورت خاص شناسایی کند تا به جای render کردن دوباره کل افراد در لیست، فقط افرادی را دوباره render کند که تغییر کرده اند.

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

class App extends Component {
  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
  }

Id می تواند هر مقداری که شما می خواهید باشد (چه حرف و چه عدد) اما مقدار آن باید یکتا باشد. حالا property یکتای key را به عناصر لیست خود اضافه می کنیم:

if (this.state.showPersons) {
      persons = (
        <div>
          {this.state.persons.map((person, index) => {
            return <Person
              click={() => this.deletePersonHandler(index)}
              name={person.name}
              age={person.age}
              key={person.id}
              />
          })}
        </div>
      )
    }

دیگر با خطای key در قسمت console مرورگر (از dev tools) مواجه نمی شویم.

سوال: آیا نمی توانیم از index برای key استفاده کنیم؟

پاسخ: شاید در نگاه اول استفاده از index مناسب به نظر برسد اما توجه داشته باشید که اگر یک عضو به آرایه اضافه شود یا یک عضو از آن حذف شود تمام عناصر index جدیدی می گیرند بنابراین index ها آنچنان هم یکتا نیستند!

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

کد Person.js به این شکل است:

const person = (props) => {
    return (
        <div className="Person">
            <p onClick={props.click}>I'm {props.name} and I am {props.age} years old!</p>
            <p>{props.children}</p>
            <input type="text" onChange={props.changed} value={props.name} />
        </div>
    )
};

در این کد رویداد onChange را داریم که نشان می دهد در زمان تغییر باید تابعی اجرا شود. فعلا props.changed برای عناصر ما وجود ندارد (آن ها را که به صورت دستی نوشته بودیم حذف کردیم تا به صورت لیستی پویا نمایش دهیم). بنابراین باید آن را نیز به عضوهای لیست اضافه کنیم:

{this.state.persons.map((person, index) => {
            return <Person
              click={() => this.deletePersonHandler(index)}
              name={person.name}
              age={person.age}
              key={person.id}
              changed={this.nameChangedHandler}
              />
   })}

بله، از nameChangedHandler استفاده کرده ایم. قبلا این متد را نوشته بودیم اما باید کمی آن را ویرایش کنیم. در حال حاضر متد nameChangedHandler به این شکل است:

  nameChangedHandler = (event) => {
    this.setState({
      persons: [
        { name: 'Max', age: 28 },
        { name: event.target.value, age: 29 },
        { name: 'Stephanie', age: 26 }
      ]
    })
  }

بنابراین یک شیء event می گیرد تا با استفاده از آن مقدار تایپ شده در input را دریافت کند (event.target.value) اما باید یک آرگومان دیگر نیز بگیرد؛ id فرد مورد نظر! دلیل آن هم واضح است؛ باید بدانیم کدام فرد در لیست ویرایش می شود. در اینجا می توانستیم از index نیز استفاده کنیم اما حالا که برای هر فرد id تعیین کرده ایم بهتر است با همان id کار کنیم:

  nameChangedHandler = (event, id) => {

اما قبل از نوشتن این کدها باید این دو آرگومان را به nameChangedHandler پاس بدهیم بنابراین:

{this.state.persons.map((person, index) => {
            return <Person
              click={() => this.deletePersonHandler(index)}
              name={person.name}
              age={person.age}
              key={person.id}
              changed={(event) => this.nameChangedHandler(event, person.id)}
            />
    })}

اگر برایتان عجیب است که چرا event را در تابع اصلی دریافت کرده ایم احتمالا ساختار arrow function ها یادتان رفته است. این arrow function مانند این است که بگوییم:

function (event) {
    this.nameChangedHandler(event, person.id)
}

در نتیجه شیء event باید ابتدا به عنوان آرگومان دریافت شود تا به متد nameChangedHandler پاس داده شود. در واقع با انجام شدن event یا رویداد ابتدا همین تابع اجرا می شود و این تابع است که شیء event را می گیرد.

حالا باید درون متد nameChangedHandler فردی که تغییر کرده را پیدا کنیم و state او را تغییر دهیم. می توانیم این کار را با متد find (از توابع جاوا اسکریپتی) انجام دهیم. همچنین تابع findIndex نیز همین کار را می کند اما index عضو پیدا شده از آرایه را برمی گرداند. ممکن است با خودتان بگویید ما می توانستیم از همان اول index را به nameChangedHandler پاس بدهیم (به جای id) و حرف شما هم درست خواهد بود. این مورد سلیقه ای است و من دوست دارم شما با این روش نیز آشنا شوید.

findIndex به عنوان پارامتر، تابعی می گیرد و آن را روی تک تک اعضا اجرا می کند (درست مانند map) بنابراین می گوییم:

nameChangedHandler = (event, id) => {
    const personIndex = this.state.persons.findIndex(p => {
        return p.id === id;
    })
}

آرگومانی که به متد findIndex داده ایم یک arrow function به صورت خلاصه است که یک آرگومان می گیرد (p). شما می توانید نام این آرگومان را هر چیزی که خواستید بگذارید. p در واقع تک تک اعضای persons در state است چرا که تابع روی تک تک این اعضا اجرا می شود بنابراین گفته ایم اگر p.id برابر با id پاس داده شده (به عنوان آرگومان به nameChangedHandler) بود نتیجه true می شود که آن را return کرده ایم. با این کار index فرد در personIndex قرار می گیرد (حواستان باشد که از findIndex استفاده کرده بودیم) بنابراین با دستور زیر می توانیم خود عنصر را به دست بیاوریم:

    const person = this.state.persons[personIndex];

در این کد ثابت person یک pointer دارد که به شیء اصلی در state اشاره می کند بنابراین نباید آن را مستقیما تغییر دهیم (ر.ک به جلسه قبل). به جای این کار باید با استفاده از spread operator یک شیء جاوااسکریپتی دیگر بسازیم:

const person = {
    ...this.state.persons[personIndex]
}

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

const person = Object.assign({}, this.state.persons[personIndex]);

حالا باید name را در این شیء جدید برابر مقدار تایپ شده قرار دهیم:

person.name = event.target.value;

سپس تمامی اعضای آرایه persons (در State) را دریافت می کنیم:

const persons = [...this.state.persons];

و در آخر مقدار عضوی را تغییر می دهیم که index اش را به دست آورده ایم:

persons[personIndex] = person;

حالا می توانیم مقدار state را تغییر بدهیم:

this.setState({ persons: persons })

بنابراین متد تکمیل شده به این شکل خواهد بود:

nameChangedHandler = (event, id) => {

    const personIndex = this.state.persons.findIndex(p => {
        return p.id === id;
    })

    const person = {
        ...this.state.persons[personIndex]
    }

    // const person = Object.assign({}, this.state.persons[personIndex]);

    person.name = event.target.value;

    const persons = [...this.state.persons];
    persons[personIndex] = person;

    this.setState({ persons: persons })
}

حالا اگر به مرورگر بروید دیگر هیچ خطایی در console نخواهیم داشت و کد ما به بهترین شکل کار خواهد کرد.

دانلود کدهای این جلسه

امیدوارم به طور کامل مبحث لیست ها را درک کرده باشید. این قسمت، قسمت پایانی فصل دوم از دوره جامع آموزش react بود بنابراین نوبت به حل تمرین می رسد! من به شما چند تمرین می دهم و از شما می خواهم که آن ها را تا جلسه بعدی حل کنید.

  1. در کامپوننت App یک input بسازید که رویدادی از نوع change داشته باشد. سپس طول (تعداد کاراکتر) رشته وارد شده در input را در پایین آن نمایش دهید (مثلا در یک عنصر p).
  2. یک کامپوننت جدید بسازید (به نام ValidationComponent) که طول رشته وارد شده در input را به صورت یک prop دریافت کند.
  3. درون کامپوننت ValidationComponent کدی بنویسید که اگر تعداد کاراکترها کمتر از 5 بود پیام «text too short» نمایش داده شود. در غیر اینصورت پیام «text long enough» به معنی «اندازه متن مناسب است» نمایش داده شود.
  4. یک کامپوننت دیگر به نام CharComponent ایجاد کنید و آن را به شکل inline استایل دهی کنید (یعنی مقدار display را روی inline-block قرار داده، padding و margin را روی 16 پیکسل تنظیم کنید، text-align را روی center بگذارید و border را روی 1px solid black قرار دهید)
  5. لیستی از CharComponent را نمایش دهید به طوری که هر کدام از CharComponent ها یک حرف از حروف وارد شده در input را به عنوان prop دریافت کنند.
  6. کاری کنید که با کلیک روی CharComponent ها، این کامپوننت ها حذف شوند.
تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

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