پاس دادن prop های ناشناخته و استفاده صحیح از State

Passing Unknown Props and Using State Correctly

23 بهمن 1399
پاس دادن prop های ناشناخته و استفاده صحیح از State

پاس دادن prop های ناشناخته

حالا که HOC خود را ساخته ایم بیایید از آن در Person.js نیز استفاده کنیم، چرا که می خواهم تک تک افراد (person) نیز دارای استایل باشند. در حال حاضر کدهای این کامپوننت به شکل زیر است:

class Person extends Component {

    render() {
        console.log('[Person.js rendering...');
        return (
            <Aux>
                <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" type="text" onChange={this.props.changed} value={this.props.name} />
            </Aux>
        );
    }

};

برای شروع کار ابتدا باید withClass را وارد Person.js کنیم:

import withClass from '../../../hoc/witchClass';

باید توجه کنید که HOC ها یک بار مصرف یا مخصوص یک کامپوننت خاص نیستند بلکه هر کامپوننتی که به کارایی ارائه شده توسط HOC نیاز داشته باشد می تواند از آن استفاده کند. در اینجا withClass کامپوننتی است که به ما اجازه استفاده از کلاس را می دهد بنابراین هر کامپوننتی که کلاس بخواهد می تواند از آن استفاده کند.

بنابراین می گوییم:

export default withClass(Person, classes.Person);

اگر الان به مرورگر برویم و صفحه را refresh کنیم متوجه دو مسئله می شوید: اولا استایل ها برگشته است، دوما نام افراد حذف شده است و هیچ داده ای نداریم! به نظر شما مشکل چیست؟

اگر به فایل withClass نگاهی بیندازید متوجه مشکل خواهیم شد:

import React from 'react';

const withClass = (WrappedComponent, className) => {
    return props => (
        <div className={className}>
            <WrappedComponent />
        </div>
    );
}

export default withClass;

تابع withClass کامپوننت درون خود را درون یک div می گذارد اما اگر به خود آن کامپوننت نگاه کنید (<<WrappedComponent) متوجه می شوید که هیچ یک از props هایش را دریافت نکرده است! ما می توانیم به صورت دستی چنین کاری انجام دهیم:

    return props => (
        <div className={className}>
            <WrappedComponent name="Max" />
        </div>
    );

اما این کار دو مشکل دارد: اولا hardcode شده است (یعنی کدها به صورت دستی و غیر پویا نوشته شده اند). دوما اگر به مرورگر بروید متوجه می شوید که تک تک افراد نامشان تبدیل به Max شده است! بنابراین این راه حل درست نیست.

اگر دقت کنید می فهمید که withClass در واقع تنها کامپوننت ما را دربرمی گیرد و ما درون فایل Persons.js تمام prop ها را به Person پاس داده و در آخر Person را درون withClass قرار می دهیم. بنابراین تمام prop های درون Persons.js در نهایت به withClass می رسند.

props درون فایل Persons.js:

    render() {
        console.log('[Persons.js] rendering...');

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

همانطور که می دانید react تمام این prop ها را یک جا و در یک شیء به نام props جمع می کند اما ما نمی توانیم بدین شکل آن را تغییر دهیم:

<WrappedComponent props={props} />

چرا؟ به این دلیل که react عادت دارد به صورت خودکار تمام property های درون JSX را گرفته و درون یک شیء به نام props قرار دهد بنابراین کد بالا فقط props را به عنوان یک خصوصیت (property) به شیء props اضافه می کند. راه حل صحیح این است که از اپراتور spread (علامت سه نقطه) استفاده کنیم؛ اگر یادتان باشد اپراتور spread یک شیء جاوا اسکریپتی را باز می کرد بنابراین می تواند شیء props ما را باز کند، تمام خصوصیت ها را بیرون بکشد و آن ها را به عنوان جفت های key=value روی این کامپوننت قرار بدهد. حالا اگر فایل را ذخیره کنید و به مرورگر بروید متوجه می شوید که مشکل ما برطرف شده است.

استفاده صحیح از setState

اگر به فایل App.js نگاه کنید حتما state خودتان را پیدا خواهید کرد. ما در این فایل از state در ری اکت به شکل صحیح استفاده کرده ایم اما راه اشتباهی هم برای استفاده از آن وجود دارد. ما می خواهیم در این قسمت این راه اشتباه را بررسی کنیم تا بعدا به این روش عمل نکنیم. فرض کنید با هر بار تغییر نام (اجرای nameChangedHandler) بخواهیم یک شمارنده داشته باشیم و تعداد تغییرات را بشماریم. برای شروع باید یک شمارنده به 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
  }

مورد اضافه شده changeCounter است. حالا به nameChangedHandler می رویم:

  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,
      changeCounter: this.state.changeCounter + 1
    });
  }

اگر دقت کنید در قسمت setState مقدار changeCounter را تغییر داده ایم. حالا اگر به مرورگر برویم و React dev tools را باز کنیم (تا بتوانیم تغییر state را مشاهده کنیم) متوجه خواهیم شد که با تایپ کردن در input شمارنده ما نیز تغییر می کند. بنابراین برنامه کاملا سالم است و هیچ مشکلی ندارد اما این روش استفاده از State در ری اکت کاملا اشتباه است!

دستور setState وضعیت state در ری اکت را به صورت لحظه ای تغییر نمی دهد بلکه آن را در لیست تغییر می گذارد، سپس هر زمان که react صفحه و State را دوباره render کند، state نیز تغییر می کند. در برنامه های کوچک مانند برنامه ما این تغییر در لحظه اتفاق می افتد (به خاطر طبیعت آن ها) اما در برنامه های بزرگ تر هیچ تضمینی وجود ندارد که State سریعا تغییر کند. شاید از خودتان بپرسید که مشکل کجاست؟ مشکل این است که در کد بالا گفته ایم:

    this.setState({
      persons: persons,
      changeCounter: this.state.changeCounter + 1
    });

یعنی برای اضافه کردن شمارنده گفته ایم State قبلی را با 1 جمع کن اما اگر setState نتواند State را در لحظه بروزرسانی کند this.state با آخرین نسخه شمارنده و state یکی نخواهد بود بلکه یک عدد قدیمی تر را به ما خواهد داد!

راه حل چیست؟ دستور SetState دو نوع پارامتر را قبول می کند: شیء و تابع. در موقعیت هایی که به آخرین نسخه state وابسته نیستیم هیچ مشکلی نیست که از حالت شیء استفاده کنیم، یعنی همین حالتی که با {} ایجاد کرده ایم. بنابراین کد زیر کاملا صحیح است:

   this.setState({
      persons: persons,
    });

اما اگر به آخرین نسخه state نیاز داریم باید از یک تابع استفاده کنیم. در این حالت دو آرگومان می گیریم: آرگومان اول prevState یا state قبلی است و آرگومان دوم props فعلی شما است (در صورتی که به آن نیاز داشته باشید). سپس درون بدنه تابع باید شیء جدید State را برگردانید:

    this.setState((prevState, props) => {
      return {
        persons: persons,
        changeCounter: this.state.changeCounter + 1
      }
    });

اما حالا به جای آنکه به this.state مراجعه کنیم می توانیم به prevState مراجعه کنیم:

    this.setState((prevState, props) => {
      return {
        persons: persons,
        changeCounter: prevState.changeCounter + 1
      }
    });

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

این دو مورد از مهم ترین مشکلاتی است که برنامه نویسان مبتدی با آن دست و پنجه نرم می کنند، بنابراین سعی کنید حتما آن ها را تمرین کنید.

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

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