مقایسه PureComponent و memo در React.js

React.js
React.js


مقدمه

مدتیه که به وضوح داره دیده میشه استفاده از React توی پروژه‌ها زیاد شده. syntax تمیز، تفکر اصولی در معماری این کتابخونه و روش قابل فهم برای دولوپرها و انعطاف پذیری React در پیاده سازی برای انواع پروژه‌ها مخصوصا پروژه‌های legacy باعث شده محبوب بشه. جا داره یه ریز به React Native اشاره کنم که خب در نوع خودش منحصر به فرد هست و با اینکه هنوز به نسخه یک نرسیده، انقدر قابلیت داره که انتخاب خیلی‌ها برای دولوپ برنامه‌های Android و iOS شده.

مدتی میشه که تیم React چند مورد جدید به کتابخونه اضافه کرده که خیلی خیلی جذاب هستن. می‌تونید برای دیدن این موارد جدید به مقاله زیر مراجعه کنید:


https://virgool.io/JavaScript8/reactjs-for-the-future-consrpouwfu6


اما توی این مقاله می‌خوام یه نگاه دقیق تری به نوشتن کامپوننت‌ها داشته باشم. نوشتن کامپوننت‌ها، در ابتدا راحت و ساده به نظر میاد ولی وقتی پروژه بزرگ و بزرگ‌تر میشه می‌بینیم که یکم پردازش زیاد میشه، مخصوصا موقع دولوپ، حتی ممکنه صفحه پروژه روی موبایل لگ‌های زیاد و اعصاب خوردکنی پیدا کنه.

صفحه وبی که با لگ شدیدی روی موبایل اسکرول می‌شه
صفحه وبی که با لگ شدیدی روی موبایل اسکرول می‌شه


این یک مثاله و مشکلات زیاد دیگه‌ای هم پیش میاد که حتی روی سیستم دسکتاپتون هم مشاهده میشه و باعث هنگ کردن مرورگرتون میشه.

واسه React Native که نگم براتون، برنامه قسمت‌های native داره که به صورت نرمال front-end کارها ازش اطلاعاتی ندارن. و باید تا جایی که می‌تونن قسمت JavaScript رو با ظرافت و تیز بینی بنویسن. ازین سبک مشکلات پرفورمنسی باعث منصرف زیاد باطری، کندی، لگ و در نهایت کرش می‌شه که واقعا دیباگش و پیدا کردن مشکلاتش فوق العاده سخته.

توی این مقاله قصد دارم توضیحاتی در مورد component lifecycle method مهمی به نام shouldComponentUpdate بدم که خیلی واضح به PureComponent و memo می‌رسیم.


فرق Component و PureComponent

زمانی بود که برای نوشتن برنامه سریع Component رو از کتابخونه React دیستراکت import می‌کردم و سریع یه کامپوننت می‌ساختم و خیلی هم خوشحال بودم.

import React, { Component } from 'react';

تا اینکه با دوستم سهیل آشنا شدم. یکبار به من گفت چرا کامپوننت رو همیشه render می‌کنی! من اولش متوجه نشدم و ایشون اشاره به مبحث Shallow Comparison کرد. با جستجوی ریزی به این رسیدم که یک lifecycle method ی وجود داره به نام shouldComponentUpdate که همیشه توی React.Component داره مقدار true بر می‌گردونه;

shouldComponentUpdate() {
    return true;
}

درسته، ما نمی‌بینیمش ولی همینه. این یعنی وقتی والدم آپدیت شد و داره رندر میشه من رو هم رندر کن. خب وقتی نیازی به این کار نیست چرا باید اینطوری بشه؟ این در صورتی هست که توی PureComponent اینطور نیست:

import React, { PureComponent } from 'react';
...
shouldComponentUpdate( nextProps, nextState ) {
    if ( notEqual( this.props, nextProps ) ) {
        return true;
    }
    if ( notEqual( this.state, nextState ) ) {
        return true;
    }
    return false;
}
...

یعنی تا زمانی که props یا state تغییری نکنه رندر دوباره‌ای در کار نیست. و این عالیه.


فرق Functional Component و Class Component

ببینید، خیلی راحته، شما هم می‌تونید یک کامپوننت ساده Hello World ری‌اکت رو هم به صورت یک function جاوااسکریپت بنویسید هم به صورت کلاس‌های ES6 ی تعریف کنید. اما این دو چه فرقی با هم دارن؟ خروجی که همون خروجی هست. برای نشون دادن فرق‌های این دو لازمه که اول سه فرق ساده رو بگم:

  1. اول از همه قیافشونه. وقتی یک قسمت کاملا نمایشی داریم که هیچ interaction ی نداره برای چی باید کد بیشتری بنویسیم. لزومی نداره.
  2. مورد بعدی بحث state هست. آره مثلا شما بخوای یه کلید سوئیچ بسازی باید state بسازی و تاگلش رو بذاری توی state ولی وقتی فقط از props یه چیزی میگیره و نمایش میده چرا باید Component بنویسیم. لزومی نداره.
  3. مبحثی هست به نام lifecycle methods که به برنامه نویس توابعی رو میده که میشه توی زمان‌های مختلف زندگی یک کامپوننت کدهایی رو اجرا کرد. مثل قبل از اتصال به VDOM، بعد اتصال، در هر آپدیت و رندر شدن و ... که این‌ها رو functional component نداریم. وقتی نیاز به این‌ها نداریم پس بازم لزومی نداره که کامپوننتمون رو کلاس طوری بنویسیم.

و اما آخری... زمانی که کامپوننت ساده Hello World رو به دو صورت می‌نویسیم و نسخه production build می‌گیریم، code دیدن داره... خودتون تماشا کنید:

DEV - function component:

import React from 'react';

const HelloWorld = () => <div>Hello World</div>;

export default HelloWorld;

DEV - class component:

import React, { PureComponent } from 'react';

class HelloWorld extends React.PureComponent {
    render() {
        return (
            <div>Hello World</div>
        );
    }
}

export default HelloWorld;

PROD - functional component

'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var HelloWorld = function HelloWorld() {
  return _react2.default.createElement(
    'div',
    null,
    'Hello World'
  );
};

exports.default = HelloWorld;

فقط نگاه کنید و ببیند که یک function ساده با الکی کلاس کامپوننت نوشتن به چه چیزی تبدیل شده:

PROD - class component

'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var HelloWorld = function (_React$PureComponent) {
  _inherits(HelloWorld, _React$PureComponent);

  function HelloWorld() {
    _classCallCheck(this, HelloWorld);

    return _possibleConstructorReturn(this, (HelloWorld.__proto__ || Object.getPrototypeOf(HelloWorld)).apply(this, arguments));
  }

  _createClass(HelloWorld, [{
    key: 'render',
    value: function render() {
      return _react2.default.createElement(
        'div',
        null,
        'Hello World'
      );
    }
  }]);

  return HelloWorld;
}(_react2.default.PureComponent);

exports.default = HelloWorld;

آره... می‌دونم باور نمی‌کنید. هر کسی باور نمی‌کنه، خودش بره به این آدرس سر بزنه، خودش امتحان کنه.

البته بگما میشه فشرده‌اش کرد و یکم این هیولا رو درستش کرد ولی همینه که می‌بینید، عمق فاجعه همینه.


بهینگی PureComponent یا سادگی function

اگر یکم فکر کنید یک سوال trade-off ی پیش میاد. یکی بهینه هستش و الکی رندر نمیشه و یکی دیگه خیلی کم حجم هست.

خوب الآن ما از کدوم استفاده کنیم؟

آخه functional component که چیزی نداره، فقط چیزهایی رو که از props ش می‌گیره رو نمایش میده. پس همیشه رندر میشه. و در کنارش کلاس کامپوننت مخصوصا PureComponent از یک lifecycle method استفاده می‌کنه که میشه باهاش از رندرهای الکی جلوگیری کرد.

چیکار باید بکنیم؟ ???


تولد memo

اینجا بود که در نسخه جدید ری‌اکت Hook ها رو وارد کردند. ابزار بسیار مفیدی که خیلی از چالش‌ها و trade-off ها رو حل کرده. memo در واقع برای functional component ها ساخته شده. تا زمانی که props ش تغیری نکنه نمیذاره که الکی توی آپدیت‌ها رندر بشه. فانکشن هم که state نداره پس داستان تمامه. کد بهینه و کم حجم.

به کد زیر دقت کنید:

import React, { memo } from 'react';

const HelloWorld = () => <div>Hello World</div>;

export default memo(HelloWorld);

و حالا نسخه پروداکشن:

'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var HelloWorld = function HelloWorld() {
  return _react2.default.createElement(
    'div',
    null,
    'Hello World'
  );
};

exports.default = (0, _react.memo)(HelloWorld);

به نظرم واضح و کامله همه چیز. چیزی برای گفتن ندارم. موفق باشید.