ری اکت باحاله بیاین باحال ترش کنیم - ۱

سلام به قسمت ۱ سری پست های «ری اکت باحاله بیاین باحال ترش کنیم» خوش اومدین. کانسپت و ایده اصلی این سری پست اینه که ما همگی ری اکت رو دوست داریم و باحاله ولی یه سری موارد(تکنیک ها) هست که میتونیم با کمک اونها Component های flexible تر و reusable بنویسیم و پس از اون میتونیم Component های باحال تری بنویسیم در نتیجه تجربه باحالتری از ری اکت داشته باشیم.

https://reactjs.org
https://reactjs.org

اولین موردی که میخوایم با هم یاد بگیریم context ئه.

واسه چی بهش نیاز داریم؟

خب هر اپلیکیشنی که با ری اکت مینویسیم مجبور میشیم یک سری داده ها رو از یک component پدری به component فرزندش پاس بدیم در اینجور مواقع اگر فرزند توی لول ۱-۲-۳ باشه ممکنه بشه کاری کرد ولی وقتی میشه لول ۷ یا این که پدر داده ای داره که اکثر فرزنداش بهش نیاز دارن از کنترل ما خارج میشه به همین خاطر روی میاریم به state manager هایی مثل redux,flux یا mobx.

این Context چی هست؟

اگه مراجعه کنیم به docs خود react اونجا context رو اینجوری تعریف میکنه که : context طراحی شده برای اشتراک گذاری داده ها به صورت global در یک سری component ها به صورت درختی مثل وضعیت احراز هویت کاربر ، قالب یا زبان مرجح.

به اشتراک گذاری به صورت درختی هم منظورش اینه شما داده ای که میخواین به اشتراک بذارین رو توی یک component پدر اعمال میکنین در تمامی component های فرزند و نتیجه و ندیده و ... بهش میتونین دسترسی داشته باشین.

چجور از context استفاده کنیم؟

خب ۲ تا api برای استفاده از context ها داریم یکیش قدیمیه و بزودی حذف میشه (نسخه ۱۷ به بالا) و اون یکی هم جدیده و از نسخه ۱۶.۳ به بعد میتونیم استفاده کنیم. ما توی این پست با هر ۲ api آشنا میشیم.

نسخه قدیم چجوری بوده ؟

ایده این api اینطور بوده که ما یک Provider(ارائه دهنده) داریم یک Receiver(گیرنده/دریافت کننده) و توی Provider که میتونه یک component پدر ساده باشه یا خودش یک component جدا باشه یک سری داده رو به عنوان context معرفی میکنیم.

Provider

 class MyProvider extends React.Component {
   getChildContext() {
    return {lang: "en"};
   }
  
    render() {
        return (<React.Fragment>{this.props.children}</React.Fragment>);
    }
  }

MyProvider.childContextTypes = {
  lang: PropTypes.string
  };

خب Provider تموم شد خیلی ساده بود نه؟ بیاین یه مرور کنیم.

چیزای جدیدی که میبینیم ممکنه اینا باشن:
توی خط ۲ متد getChildContext() رو داریم که مربوط به خود react ئه و هرچی که این متد برگردونه (طبیعتا نیازه که object برگردونده بشه) میشه context ما و این context رو بین تمام child هاش share میکنه.
توی خط ۷ Fragment رو داریم (که از ری اکت ۱۶.۲ اضافه شد) همونطور که میدونین ما مجبوریم توی react در هر component فقط یک element یا یه component رو برگردونیم و اگر تعداد component یا elemnt هامون بیشتر از یکی باشه ری اکت ارور میده و ما هم معمولا برای رفع این مشکل میومدیم و اونهارو توی یک div میذاشتیم ولی بعضی اوقات اون div اضافه بود و style مون توی css رو خراب میکرد و Fragment راهکاریه که خود react فراهم کرده تا زمانی که به همچین مشکلی خوردیم از div یا موارد مشابهش استفاده نکنیم.
*: میشد کد خط ۷ رو با کد زیر جایگزین کرد و مشکلی هم پیش نیاد ولی میخواستم Fragment رو بگم که باهاش آشنا بشیم.

return (this.props.children)

در اخر هم childContextTypes رو داریم که تایپ(نوع داده) های context رو مشخص میکنیم. که میگیم در خط بعدیش میگیم این context یه lang داره که تایپش string ئه.

*: بعد از ری اکت ۱۵.۵ PropTypes یه پکیج مستقل شده و برای استفاده ازش میتونین با دستور زیر اونو به dependency هاتون اضافه کنین:

 npm i prop-types 

**: اگر از es7 در کداتون استفاده کنین میدونین که میشه عوض این که childContextTypes هارو جدا گونه و خارج از کلاس اضافه کنین میتونین به صورت static و داخل class باشه. بدین شکل:

static childContextTypes = {
    lang: PropTypes.string,
 }

Receiver

و کد قسمت Receiver بدین شکل میشه.

class MyReceiver extends React.Component {
    render() {
        const { lang} = this.context
        return (
        `The Language is ${lang}`
        );
     }
}
MyReceiver.contextTypes = {
    lang : PropTypes.string
};

در Receiver خیلی ساده اول contextTypes رو میگیم که چه context ئی رو میخوایم و سپس در کلاس component مون با this.context بهش دسترسی داریم.

*:اگر از es7 در کداتون استفاده کنین میدونین که میشه عوض این که contextTypes هارو جدا گونه و خارج از کلاس اضافه کنین میتونین به صورت static و داخل class باشه. بدین شکل:

static contextTypes = {
    lang: PropTypes.string,
 }

تمرین

برای تمرین در این مورد بیاین یه چیزی شبه Provider,connector پکیج react-redux بنویسیم.خب Provider این پکیج یه prop به عنوان store میگیره که store شامل همه ی داده هایی هست که ممکنه در اپلیکیشن نیاز داشته باشیم به اشتراک گذاشته بشن و با کمک connector اش در هر component فرزندش میتونه به store و داده هایی که نیاز داره دسترسی داشته باشه و اون داده هارو به صورت prop دریافت میکنه.

قبل شروع نیاز هست با HOC آشنا بشیم.

این HOC چیه؟

خب HOC مخف Higher Order Component ئه و برخلاف اسم سختش درک کاری که میکنه آسونه . کار HOC اینه که شما بهش یه Component میدین و اون هم یه Component دیگه برمیگردونه.

خب به چه دردی میخوره؟

مثلا شما میخواین همه ی prop هایی که به component تون میدن رو میخواین لاگ شو ببینین.
یا مثلا component شمارو میگیره و اگر توی context دید که کاربر اعتبار سنجی شده component ئی که بهش دادیم رو برمیگردونه و اگر کاربر اعتبار سنجی نشده بود بهش component ئی که اعلام خطا میکنه به کاربر رو برمیگردونه و یا مثلا پکیج های routing یک hoc ئی رو در اختیار شما قرار میدن که اگر component شما نیاز به اطلاعات route داشت رو به صورت prop به component شما اضافه میکنه.

اصلا بیاین مثال اولیه رو بنویسیم:

const withLog = (Component)=> {
  const WithLogComponent = (props) => {
     console.group(`Component ${Component.name} Props`);
     console.log(props);
     console.groupEnd()
     return React.createElement(Component,props);
    }
 return WithLogComponent ;
}

خب اول یه فانکشن داریم که component رو میگیره بعد یک stateless component میسازیم که props رو کنسول لاگ میکنیم. با کمک React.createElement یه component جدید میسازیم و در نهایت این component رو برمیگردونیم.
*: به جای React.createElement هم میتونیم ازین استفاده کنیم:

return (<Component {...props} />)

برگردیم سر تمرینمون.

OurAwesomeProviderCreator

function createProvider(store) {
    return class Provider extends React.Component {
       static childContextTypes = {
         store: PropTypes.object,
       }
       state = store
       getChildContext() {
          return { store: this.state }
       }

       render() {
          return (this.props.children)
       }
    }
}

خب یه فانکشن نوشتیم که بهش یه ابجکت میدیم اونم یه Provider برمیگردونه که اون ابجکته میشه context مون چیز جدیدی نیست میریم سراغ connect مون.

Connect

function connect(mapStateToProps) { 
 return Component => {
 return class MyReceiver extends React.Component {
  static contextTypes = {
      state: PropTypes.object
   };
  render() {
   const { state } = this.context
   const mappedProps = mapStateToProps(state)
   return (
        <Component {...mappedProps} {...this.props} />
   );
   }
 }
 }
}

اینجا هم چیز جدیدی ندارم یه فانکشن داریم که یه فانکشن به اسم mapStateToProps رو میگیره و یه hoc برمیگردونه کار hoc هم اینه که یه component برمیگردونه که prop هایی رو که ما مشخص کردیم رو به component اصلی که میخوایم پاس میده.
در اصل mapStateToProps یه فانکشنه که state یا context رو میگیره و یه ابجکت برمیگردونه که این ابجکت به عنوان props به component ئی که میخوایم پاس داده میشه دلیل این کار اینه که ممکنه component ما نیاز به prop ئی داشته باشه که توی context هم با همون نام داشته باشیمش پس با کمک mapStateToProps میایم میگیم درسته که توی context ما color رو داریم ولی میخوایم به عنوان prop ئی به نام contextColor بگیریمش.


طریقه استفاده از Provider و connect مون هم اینجوریه:

const store = {
  color: 'black'
}
const OurColorProvider = createProvider(store)

const FirstComponent = (props) => (<div>
    I got 2 color {props.color} & {props.contextColor}
  </div>
)
const ConnectedFirstComponent = connect(
  (state) => ({contextColor: state.color})
)(FirstComponent);
const App = (props) => (
  <OurColorProvider>
    <ConnectedFirstComponent color="white" />
  </OurColorProvider>
)
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

نسخه جدید چطور شده ؟

تا قبل از این نسخه ری اکت این قابلیت رو آزمایشی در نظر میگرفت و پیشنهاد میکرد ازش استفاده نکنن ترجیحا یا انقد حرفه ای باشن که بدونن دارن چی کار میکنن .از ری اکت ۱۶.۳ این روندی که طی میکردیم بسیار آسون تر شده . اول باید یک context ایجاد کنیم.

 const MyProfileContext = React.createContext();

نکته : createContext میتونه یه پارامتر به نام defaultValue رو هم میگیره که در موردش پایین تر میگم.

دوم یه component میخوایم که از Provider ئی که createContext در اختیارمون استفاده کنیم و باید value که میخوایم context داشته باشه رو از طریق prop بهش بدیم:

 
class MyProfileProvider extends Component {
 state = {
    name: 'Arash',
    age: 26,
    like : 10,
    twitter: '@arainjast'
  }
 render() {
 return (
      <MyProfileContext.Provider value={{
        state: this.state,
      }}>
       {this.props.children}
      </MyProfileContext.Provider>
 )
  }
}

مرحله سوم وقتی در یکی از فرزندان Provider بخوایم ازین مقادیر استفاده کنیم میتونیم از Consumer استفاده کنیم. بدین شکل :

class Profile extends Component {
 render() {
 return (
        <MyProfileContext.Consumer>
               {(context) => (
                     <React.Fragment>
                           <p>Name: {context.state.name}</p>
                            <p>Age: {context.state.age}</p>
                            <p>Like: {context.state.like}</p> 
                      </React.Fragment>}
                 )}
        </MyProfileContext.Consumer>
 )
  }
}

شما باید به Consumer یک فانکشن به عنوان child پاس بدید که این فانکشن context رو در اختیارتون میزاره.

نکات :

۱- وقتی ری اکت Consumer رو رندر میکنه مقدار context رو از اولین و نزدیک ترین Provider میخونه. و اگه Provider ئی رو پیدا نکرد مقدار context رو برابر defaultValue قرار میده.

۲- فانکشن به عنوان child وقتیه که شما به جای یک element یا component از فانکشن استفاده کنین. مثلا Consumer اینجا به صورت خیلی خیلی سادش یه همچین شکلی داره:

class Consumer extends React.Component {
 static propTypes = {
    children: React.PropTypes.func.isRequired,
  }
 getContextValue(){
 //...
  }
 render() {
 const context = this.getContextValue()
 return (
      <div>
 {this.props.children(context)}
      </div>
 );
  }
}

از اونجا که children یک فانکشنه که خروجیش یه المان یا component ئه باید فانکشن اجرا بشه تا یه تایپ مجاز بشه که ری اکت بتونه رندرش کنه.

۳- در اکثر مواقع نیاز میشه که مثلا مقادیر context رو آپدیت کنیم مثلا قالب رو light به dark . در این مواقع به راحتی میتونیم فانکشنی در context داشته باشیم که مقادیر مورد نظرمون رو اپدیت کنه. در این مثال پروفایل که داشتیم ممکنه بخوایم دکمه لایک و دیس لایک رو اضافه کنیم. برای این کار اول در قسمت Provider این تغییرات رو میدیم :

<MyProfileContext.Provider value={{
      state: this.state,
      like: () => this.setState({
        like: this.state.like+ 1
      })‌,
      dislike: () => this.setState({
        like: this.state.like - 1
      }) 
    }}>

حال در قسمت Consumer :

<MyProfileContext.Consumer>
 {(context) => (
    <React.Fragment>
      <p>Name: {context.state.name}</p>
      <p>Age: {context.state.age}</p>
      <p>Like: {context.state.like}</p>
      <button onClick={context.like}>👍</button>
      <button onClick={context.dislike}>👎</button>
    </React.Fragment>
 )}
</MyProfileContext.Consumer>



ممنون از این که این همه مطلب رو خوندین و امیدوارم که خوب درکش کرده باشین. این راه رو ادامه میدیم تا بتونیم هرروز ری اکت باحال تری داشته باشیمو Component های باحال تری بنویسیم. منتظر تکنیک های باحال تر باشین.