state و lifecycle

این صفحه مفهوم state و lifecycle را در یک کامپوننت ری‌اکتی معرفی می‌کند. برای مطالعه مرجع API کامپوننت با جزئیات به اینجا مراجعه کنید.

مثال ساعت را در یکی از بخش‌های پیشین در نظر بگیرید. در رندر کردن المنت‌ها، ما تنها یک راه برای به‌روز رسانی UI یاد گرفتیم. ما تابع ReactDOM.render() را فراخوانی می‌کنیم تا خروجی رندر شده را تغییر دهیم:

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

روی ‌CodePen امتحان کنید

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

می تونیم با کپسوله کردن ظاهر ساعت شروع کنیم:

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

روی ‌CodePen امتحان کنید

اگرچه، یک نیاز حیاتی را از دست می‌دهد: این که Clock یک تایمر را تنظیم و هر ثانیه UI را به‌روز‌ رسانی کند، که باید بخشی از جزئیات پیاده‌سازی خود Clock باشد.

ایده‌آل این است که ما یک بار Clock را بنویسیم و خودش به‌روز رسانی را انجام دهد:

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

برای پیاده‌سازی آن، باید به کامپوننت Clock state اضافه کنیم.

state مشابه props است، اما خصوصی است و کاملا توسط کامپوننت کنترل می‌شود.

تبدیل یک تابع به یک کلاس

شما می‌توانید یک کامپوننت برپایه تابع را در پنج مرحله به یک کلاس تبدیل کنید:

  1. یک کلاس ES6 بسازید، با همان نام که از React.Component ارث می‌برد.

  2. یک متد خالی با نام render() به آن اضافه کنید.

  3. بدنه تابع را به متد render() منتقل کنید.

  4. در بدنه render() به‌جای props، this.props بنویسید.

  5. آن‌چه از تعریف تابع خالی باقی مانده‌است را پاک کنید.

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

روی CodePen امتحان کنید

حالا Clock به جای تابع، به صورت کلاس تعریف شده‌است.

هربار که به‌روز رسانی اتفاق می‌افتد، تابع render فراخوانی می‌شود. اما تا وقتی که ما <Clock /> را درون همان DOM node رندر می‌کنیم، تنها یک نمونه از کلاس Clock استفاده خواهدشد. این باعث می‌شود ما قادر به استفاده از ویژگی‌های دیگری مانند state و توابع lifecycle باشیم.

اضافه‌کردن state داخلی به یک کلاس

ما در سه مرحله، date را از props به state انتقال می‌دهیم:

  1. در تابع render()، this.props.date را با this.state.date جایگزین کنید:
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
  1. یک سازنده کلاس اضافه کنید که مقداردهی اولیه this.state را انجام می‌دهد:
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

آگاه باشید که ما چگونه props را به سازنده پدر پاس می‌دهیم:

  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

کامپوننت‌های بر پایه کلاس باید همیشه سازنده پدر را با props فراخوانی کنند.

  1. date را از props المنت <Clock /> حذف کنید:
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

ما بعدا کد مربوط به تایمر را به خود کامپوننت اضافه‌می کنیم.

نتیجه این شده‌است:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

روی CodePen امتحان کنید

در ادامه، Clock را طوری تغییر می‌دهیم که تایمر خودش را داشته‌باشد و خودش هر ثانیه به‌روز رسانی شود.

افزودن توابع lifecycle به یک کلاس

در برنامه‌هایی با تعداد کامپوننت زیاد، بسیار اهمیت دارد که با از بین رفتن کامپوننت، منابعی که توسط آن اشغال شده‌بود نیز آزاد شود.

ما می خواهیم یک تایمر را برای اولین دفعه‌ای که Clock در DOM رندر می‌شود، تنظیم کنیم. در ری‌اکت به آن “mounting” گفته می‌شود.

همچنین می‌خواهیم هر زمان که DOM تولید شده توسط Clock حذف می‌شود، تایمر را پاک کنیم. در ری‌اکت به آن “unmounting” گفته می‌شود.

برای اجرای کد‌هایی زمان mount و unmount شدن یک کامپوننت، ما می توانیم توابع ویژه‌ای روی کامپوننت‌های بر پایه کلاس تعریف کنیم:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {

  }

  componentWillUnmount() {

  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

به آن‌ها “توابع lifecycle” گفته می‌شود.

تابع componentDidMount() پس از رندر شدن خروجی کامپوننت توی DOM، اجرا می‌شود. اینجا محل خوبی برای تنظیم یک تایمر است:

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

دقت داشته‌باشید که چگونه ما شناسه تایمر را دقیقا روی this (this.timerID) ذخیره می‌کنیم.

در حالی که this.props توسط خود ری‌اکت تنظیم می‌شود و this.state کاربرد خاص خودش را دارد، شما آزاد هستید که برای ذخیره چیزی که نقشی در جریان داده‌ها ندارد (مانند شناسه تایمر)، به صورت دستی فیلد‌های دیگری به کلاس اضافه کنید.

ما تایمر را در تابع lifecycle componentWillUnmount() از کار می‌اندازیم:

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

در آخر، ما تابعی با نام tick() پیاده‌سازی می‌کنیم که کامپوننت Clock هر ثانیه آن‌را فراخوانی می‌کند.

این [تابع] از this.setState() استفاده می کند تا به‌روز رسانی‌ها را روی state داخلی کامپوننت انجام دهد:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

روی CodePen امتحان کنید

حالا ساعت هر ثانیه تغییر می‌کند.

بیایید به طور خلاصه جمع‌بندی کنیم که چه اتفاقی ره می‌دهد و به ترتیب چه توابعی فراخوانی می‌شوند:

  1. زمانی که <Clock /> به ReactDOM.render() پاس داده می‌شود، ری‌اکت سازنده کامپوننت Clock را فراخوانی می‌کند. از آن‌جایی که Clock زمان جاری را نمایش دهد، this.state را با شیء‌ای شامل زمان جاری مقداردهی اولیه می‌کند.

  2. سپس ری‌اکت تابع render() کامپوننت Clock را فرا می‌خواند. این روشی است که ری‌اکت می‌فهمد چه چیزی باید روی صفجه نمایش‌داده شود. سپس ری‌اکت DOM را به‌روز رسانی و با خروجی رندر Clock تطبیق می‌دهد.

  3. زمانی که خروجی Clock به DOM اضافه می‌شود، ری‌اکت تابع lifecycle componentDidMount() را فراخوانی می‌کند. درون آن، کامپوننت Clock از مرورگر می‌خواهد که یک تایمر تنظیم کند که هر ثانیه تابع tick() را فراخوانی کند.

  4. مرورگر هر ثانیه تابع tick() را فرا می‌خواند. درون آن، کامپوننت Clock به‌روز رسانی UI را با فراخوانی تابع setState() همراه یک شیء شامل زمان جاری، زمان‌بندی می‌کند. ری‌اکت با کمک setState() متوجه می‌شود که state تغییر کرده‌است. و آن‌گاه برای اطلاع از آن‌چه باید روی صفحه نمایش‌ داده‌شود، تابع render() را فرا می‌خواند. این دفعه، مقدار this.state.date در تابع render() متفاوت خواهد بود و بنابراین خروجی رندر دارای زمان به‌روز رسانی شده خواهدبود. ری‌اکت DOM را بر همین اساس به‌روز‌ رسانی می‌کند.

  5. اگر کامپوننت Clock از DOM حذف شود، ری‌اکت هم تابع lifecycle componentWillUnmount() را فرا می‌خواند و در نتیجه تایمر متوقف می‌شود.

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

سه چیز هست که باید درباره setState() بدانید:

مستقیم state را تغییر ندهید

برای مثال، این کار باعث رندر مجدد یک کامپوننت نمی‌شود:

// اشتباه
this.state.comment = 'Hello';

به جای آن از setState() استفاده کنید:

// درست
this.setState({comment: 'Hello'});

تنها جایی که شما می‌توانید this.state را [مستقیم] مقدار‌دهی کنید، سازنده [کلاس] است.

به‌روز رسانی state ممکن است غیرهمزمان باشد

ممکن است ری‌اکت برای بهبود عملکرد، فراخوانی چند باره setState() را در یک به‌روز رسانی انجام دهد.

از آن‌جا که ممکن است this.props و this.state به صورت غیرهمزمان به‌روز ‌رسانی شوند، برای محاسبه وضعیت بعدی نباید روی مقادیر آن‌ها حساب کنید.

برای مثال، این کد ممکن است در به‌روز رسانی شمارنده دچار اشکال شود:

// اشتباه
this.setState({
  counter: this.state.counter + this.props.increment,
});

برای حل آن، شکل دوم تابع setState() را استفاده کنید که به جای یک شیء، یک تابع به عنوان ورودی می‌پذیرد. آن تابع هم state قبلی را به عنوان ورودی اول، و props مربوط به زمانی که تعییرات اعمال شده‌است را به عنوان ورودی دوم دریافت می‌کند.

// درست
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

در بالا ما از تابع arrow استفاده کرده‌ایم، اما با تابع معمولی هم کار می‌کند:

// درست
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

به‌روز رسانی‌های state ادغام‌شده هستند

وقتی شما setState() را فرا می‌خوانید، ری‌اکت آن شیء را در state فعلی ادغام می‌کند.

برای مثال، state شما ممکن است دارای چندین متغییر مستقل باشد:

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }

پس شما می‌توانید آن‌ها را جدا از هم، با فراخوانی‌های مجزای setState()، به‌روز رسانی کنید:

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

این ادغام سطحی است، بنابراین this.setState({comments}) تاثیری روی this.state.posts ندارد، اما کاملا this.state.comments را جایگزین می‌کند.

داده به پایین جریان دارد

نه کامپوننت پدر و نه کامپوننت فرزند از این‌که یک کامپوننت مشخص دارای state است یا نه خبری ندارند و نباید برای آن‌ها مهم باشد که به صورت تابع یا کلاس تعریف شده‌است.

به همین دلیل است که اغلب state را داخلی یا کپسوله‌شده خطاب می‌کنند. به غیر از کامپوننتی که مالک آن است و با آن کار می کند، توسط هیچ کامپوننت دیگری قابل دسترسی نیست.

یک کامپوننت ممکن است خودش انتخاب کند که state خود را به عنوان props به کامپوننت‌های فرزند انتقال دهد.

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

همچنین برای کامپوننت‌هایی که توسط کاربر تعریف شده‌است، کار می کند.

<FormattedDate date={this.state.date} />

کامپوننت FormattedDate از props خود date را دریافت می‌کند و نخواهد فهمید که از state یا props Clock به دریافت کرده، یا با دست تایپ شده‌است.

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

روی CodePen امتحان کنید

به این جریان داده عموما “یک طرفه” یا “بالا به پایین” گفته می‌شود. مالکیت هر state در دست یک کامپوننت مشخص است و هر داده یا UI که از آن state مشتق شده‌باشد، فقط کامپوننت‌های زیرین خود را در ساختار درختی تحت تاثیر قرار می‌دهد.

اگر یک درخت کامپوننت را به شکل آبشاری از props ها تصور کنید، state هر کامپوننت مانند منبعی اضافه از آب است که در نقطه‌ای دلخواه به آن متصل و به پایین جریان پیدا می‌کند.

برای نشان‌دادن این‌که تمام کامپوننت‌ها واقعا ایزوله هستند، ما می‌توانیم یک کامپوننت App بسازیم که سه کامپوننت <Clock> را رندر می‌کند.

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

روی CodePen امتحان کنید

هر Clock تایمر خودش را تنظیم و به طور مستقل به‌روز رسانی می‌کند.

در برنامه‌های ری‌اکتی، این‌که یک کامپوننت دارای state هست یا نه، به جزئیات طراحی آن کامپوننت مربوط می‌شود که ممکن است در طول زمان تغییر کند. شما می‌توانید کامپوننت‌های بدون state را درون کامپوننت های دارای state استفاده کنید و همچنین برعکس.