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

تو روند توسعه اپلیکیشنمون از تکنیک ها و پترن های مختلفی استفاده می کنیم در قسمت قبل با Context و HOC آشنا شدیم. اگه قسمت قبلی رو نخوندین پیشنهاد میکنم قسمت قبل رو قبل این که این قسمتو بخونین اینجا بخونین.

https://virgool.io/JavaScript8/%D8%B1%DB%8C-%D8%A7%DA%A9%D8%AA-%D8%A8%D8%A7%D8%AD%D8%A7%D9%84%D9%87-%D8%A8%DB%8C%D8%A7%DB%8C%D9%86-%D8%A8%D8%A7%D8%AD%D8%A7%D9%84-%D8%AA%D8%B1%D8%B4-%DA%A9%D9%86%DB%8C%D9%85-%DB%B1-pycysgrv64wi

این قسمت میخوایم با Compound Components آشنا بشیم. Compound Components ها به ما این امکانو میدن که Component ئی که طراحی میکنیم خیلی قابل انطعاف باشن.

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

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

مثلا فکر کنین یه Component جدول(Table) داریم تهیه میکنیم که یک Input داریم که میتونیم در جدول جستجو کنیم یک روش خیلی ساده اینه که Input رو بالای جدول قرار میدیم ولی مشکل اینجاست که اگر نیاز شد این Input پایین جدول باشه چی؟ اگر یجایی نخواستیم که جستجو داشته باشه ؟ اگر بخوایم جستجو تو کامپوننت Header مون باشه خود جدول تو یه کامپوننت دیگه چی؟
فکر کنم فهمیدین منظورم چیه Compound Components دقیقا همین اجازه رو به ما میدن که بتونیم Component مون رو انعظاف پذیرتر و قابل استفاده مجدد (reusable) کنیم.

پیاده سازی یک مثال ساده

خب برای اشنایی با این روش میایم یه مثلا ساده میزنیم فک میکنیم که کامپوننتی به نام Toggleداریم این کامپوننت شامل یک دکمه روشن/خاموش و ۲ کامپوننت دیگه هست که بتونیم به صورت شرطی با توجه به وضعیت on/off بودنش محتوایی که میخوایم رو نشون بدیم و شکل نهایی کار بدین شکله :

<Toggle> 
    <Toggle.On>الان روشنه</Toggle.On>
    <Toggle.Off>الان خاموشه</Toggle.Off>
    <Toggle.Switch/>
</Toggle>

ما در اینجا میتونیم Toggle.On رو ببریم پایین سوییچ یا اصلا برش داریم و تاثیری توی کارکرد نداره.
خب اول یه کامپوننت ساده داریم :

class Toggle extends React.Component {
    state = {
        on : false
    }
    toggle = () => {
        this.setState({on : !this.state.on})
    }
    render(){
        return (<div onClick={this.toggle}>{this.state.on ? 'ON' : 'OFF'}</div>)
    }
}

خب تا این قسمت چیز عجیبی که نداریم یه کامپوننت ساده که یه state داره که مشخص میکنه الان on هست یا off و رندر که وضعیتو نشون میده.
حالا میخوایم ۲ تا کامپوننت تعریف کنیم که به صورت شرطی نشون بدیم وضعیتو :

static On = props => props.on ? props.children : null;
static Off = props => props.on ? null : props.children;

برا این که بتونیم کامپوننت هامون رو به صورت Toggle.On و Toggle.Off دربیاریم باید از static استفاده کنیم. اضافه کردن static اول هر property باعث میشه که اون property تبدیل به یک static property بشه و static property مال خود class هست و به شکل Class.static در دسترسند و instance های کلاس دسترسی به این property ها ندارند.
حال نوبت کامپوننت Switch هست.

static Switch = props => (<div className="switch" onClick={props.toggle}>Turn {props.on?'OFF':'ON'}</div>); 

تا اینجا کد به شکل زیر میشه :

 class Toggle extends React.Component {
      state = { 
              on : false     
      }
       static On = props => props.on ? props.children : null;
       static Off = props => props.on ? null : props.children; 
       static Switch = props => (<div className="switch" onClick={props.toggle}>Turn {props.on?'OFF':'ON'}</div>); 
      toggle = () => {
               this.setState({on : !this.state.on})     
      }
      render(){
               return (<div onClick={this.toggle}>{this.state.on ? 'ON' : 'OFF'}</div>)     
      }
  } 

تنها مرحله باقی مانده ما اینه که کاری کنیم که وقتی این ۳ کامپوننت به عنوان child های کامپوننت اصلی قرار گرفتند prop هارو دریافت کنن بنابر این قسمت رندر رو بدین شکل مینویسیم.

render(){
    return React.Children.map(this.props.children, child => { return React.cloneElement(child, {                 on: this.state.on, toggle: this.toggle }) }) 
}

همونطور که میدونین میتونیم به children های یک کامپوننت با this.props.children دسترسی داشته باشیم و برای این که بتونیم prop هایی رو به تک تک child ها پاس بدیم میایم از React.Children.map استفاده میکنیم. این متد پارامتر اولش children هاست و هر children رو میاد به فانکشنی که از پارامتر دوم گرفته پاس میده.
ازون طرف React.cloneElement داریم که پارامتر اول یک المنت و پارامتر دوم prop های اضافی هست که میتونین بهش پاس بدین. اینجا با کمک React.Children.map و این به همه children های مستقیم prop هایی که نیاز بود که on و toggle هستن رو به prop هاشون اضافه میکنیم.

خب کار این کامپوننت تمومه و تو این مثال با حالت ساده Compound Components آشنا شدیم. دراین حالت ما یک مقدار انعطاف پذیری داریم ولی اون انعطاف پذیری که میخوایم رو نداریم آیا میتونین بگین که مشکل چیه در این حالت ؟ یه مقدار بررسی کنین ببینین مشکل رو میتونین پیدا کنین اگر نتونستین پاراگراف بعدی مشکلو میفهمیم.

مشکل چیه؟

مشکل دقیقا جاییه که من ۲ تا پاراگراف بالاتر bold اش کردم که تو این روش ما prop هارو تنها میتونیم به همه children های مستقیم پاس بدیم برای مثال شاید نیاز باشه که که یکی از کامپوننت هارو توی یه div بزاریم که یه سری استایل های اضافه تر بهش بدیم اونوقت با این روش کار نمیکنه چون اگر یکی از این کامپوننت ها توی div قرار بگیره اونوقت وقتی داریم cloneElement میکنیم prop هارو میدیم به div ولی div این prop هارو به child هایی پایینیش نمیده در نتیجه کامپوننت ما به prop ئی که نیاز داره دسترسی نداره و تمام. کارکردی که میخوایم رو نخواهد داشت.

چجوری مشکل رو رفع کنیم ؟

حتما حدس زدین که مشکل رو چی میتونه حل کنه ؟.... درسته context اصلا برای همین یه قسمت کامل در موردشون تویه قسمت قبل صحبت کردیم. کامپوننت پدر اینجا (Toggle ) اینجا نقش یه والد(parent) رو داشت حالا اگر بیایم ازش به عنوان provider استفاده کنیم چی میشه ؟‌ تمام prop هایی که میخوایم در هر سطحی نسبت به provider باشن قابل دسترسی اند. و نیازی هم دیگه به cloneElement و React.Children.map نیست.

مثال نهایی

اول این قسمت در مورد یه کامپوننت Table صحبت کردیم همینو به عنوان مثال نهایی با Compound Components و Context پیاده سازی میکنیم. نام کامپوننتمون رو میزاریم XTable

دمو چیزی که میسازیم رو میتونین اینجا ببینین .(این سایت متاسفانه ایران رو تحریم کرده 😐 قبل از ورود به سایت جهت مشاهده حتما موارد لازمه رو رعایت کنین.)

۱- یک فایل به نام XTableContext.js میسازیم که context مون هست.

import React from "react";
const XTableContext = React.createContext();
export default XTableContext;

۲-یک فایل به نام XTable.js میسازیم که کامپوننت اصلیمون هست.

 import React from "react";
import Context from "./XTableContext";
import Table from "./Table";
import Search from "./Search";

export default class XTable extends React.Component {
 state = {
    query: ""
  };
 setQuery= e => {
      this.setState({ query: e.target.value });
  };

 static Table = Table;
 static Search = Search;
 render() {
 const { columns, rows} = this.props;
 const { query } = this.state
 const context = {
      data: {
      columns,
      query,
        rows: rows.filter(row=>{
         return Object.keys(row).some(column=>{
            return (row[column]+"").includes(query)
          })
        }),
      },
      action: {
        setQuery: this.setQuery
      }
    };
 return (
      <Context.Provider value={{ ...context }}>
 {this.props.children}
      </Context.Provider>
 );
  }
}

خب در کامپوننت XTable یه state داریم به نام query که میخوایم که مقداری که میخوایم table رو بر اساس اون filter کنیم رو نگه داری میکنیم.
مقدار query توسط کامپوننت Search و input ئی که داره باید خونده بشه و با متد setQuery این اجازه رو میدیم که مقدار input رو در state مون ذخیره کنیم.
۲ تا کامپوننت Table و Search داریم که بزودی میریم سراغ اونا کارشون معلومه.
۲ تا props کامپوننت XTable مون میگیره یکی columns هست یکی rows که ستون و سطر های جدولمون هستند . این مقادیر به صورت آرایه به کامپوننت ما باید پاس داده بشن. و شکلی مشابه زیر دارن:

const columns = [
  { key: 'site', title: 'Sites' },
  { key: 'view', title: 'Views' },
  { key: 'click', title: 'Clicks' },
  { key: 'average', title: 'Average' },
  ]
 const rows = [
 {
   site: 'Google',
   view: 9518,
   click: 6369,
   average: '01:32:50',
   },
   {
   site: 'Twitter',
   view: 7318,
   click: 11369,
   average: '02:12:45',
   },
   {
     site: 'Amazon',
     view: 4918,
     click: 3253,
     average: '00:32:52',
    },
    {
     site: 'LinkedIn',
     view: 3518,
     click: 2369,
     average: '00:12:45',
     },
    {
      site: 'CodePen',
      view: 2228,
      click: 1048,
      average: '00:08:56',
     },
    {
      site: 'GitHub',
      view: 4521,
      click: 3732,
      average: '00:36:51',
      },
]; 

هر آبجکت ستون شامل یک key که key مربوط به اون سطر در ابجکت های هر سطر و یک title که عنوان ستون در هنگام نمایش هست.

ابجکت context شامل data , action میشه که data مقادیری که context به اشتراک میذاره و action شامل فانکشن هایی هست که در context به انها نیاز داریم اینجا تنها نکته ی این قسمت ، کد زیره :

rows.filter(row=>{
                  return Object.keys(row).some(column=>{
                                    return (row[column]+"").includes(query)
 })

ما نیاز داریم که سطر هایی را نشان کاربر بدهیم که برای انها سرچ کرده در نتیجه نیاز داریم از filter کمک بگیریم. خلاصه کد بالا به زبون فارسی میشه : سطر هایی رو به من بده(rows.filter) که حداقل یکی از key هاش(Object.keys(row).some) شامل این مقدار باشن (row[column]+"").includes(query) دلیلی که مقدار row[column] را با "" جمع کردیم این هست ممکنه مقدار ستونی که میخوایم درش جستجو کنیم عدد باشه و اعداد دارای متد includes نیستن و با جمعشون با "" انها را به رشته تبدیل میکنیم.
باقی کد ها هم نکته ای ندارند.

۳- یک فایل Search.js داریم که کامپوننت سرچمون هست:

 import React from "react";
 import Context from "./XTableContext";
 const Search = () => (
 <div >
  <Context.Consumer>
  {context => (
   <input type="text"
    value={context.data.query}
    onChange={context.action.setQuery}
   />
   )}
  </Context.Consumer>
 </div>
);
export default Search;

این کامپوننت سادست و نیازی به توضیحی نداره.

۴- کامپوننت Table و فایل Table.js

import React from "react";
import Context from "./XTableContext";
import Thead from "./Thead";
import Tbody from "./Tbody";
const Table = () => (
 <table >
  <Thead />
  <Tbody />
 </table>
);
export default Table;

ساده ترین کامپوننتی که میتونیم داشته باشیم!‌😂

۵- کامپوننت Thead برای نمایش heading در Table که در فایل Thead.js :

import React from "react";
import Context from "./XTableContext";

const Thead = () => (
    <thead>
        <tr>
        <Context.Consumer>
            {context =>
                context.data.columns.map(column => (
                    <th key={column.key}>
                        {column.title}
                    </th>
                ))
              }
          </Context.Consumer>
          </tr>
    </thead>
 );
 export default Thead;

یه کامپوننت ساده دیگه😁

۶- و آخرین کامپوننتمون Tbody و فایل Tbody.js :

import React from "react";
import Context from './XTableContext'

const Tbody = () => (
  <tbody>
    <Context.Consumer>
 {context => context.data.rows.map(row => (
        <tr key={row.sites}>
         {
           context.data.columns.map(column=> (
             <td key={row[column.key]}>{row[column.key]}</td>
             ))
           }
         </tr>
    ))}
    </Context.Consumer>
  </tbody>

)
export default Tbody;

اینم کامپوننت ساده ایه با Consumer رفتیم context رو گرفتیم و row های رو توی یه map ستون ها انداختیم ستون هارو به ترتیبشون قرار دادیم.


خب این قسمت هم تموم شد. تو این قسمت یاد گرفتیم که چه ساده میتونیم کامپوننت های بهتر و باحال تر و انعطاف پذیر ترو قابل استفاده ی مجددتری بنویسیم. این راه رو ادامه میدیم تا بتونیم هرروز ری اکت باحال تری داشته باشیم ...


*با تشکر از آقای ‌PABLO GARCÍA FERNÁNDEZ که این قالب باحال css برای جدولو زده.