Стан (State) і жыцьцёвы цыкл (Lifecycle)

Гэтая старонка ўводзіць паняцьце стану (state) і жыцьцёвага цыкла (lifecycle) у кампанэнце React. Вы можаце знайсьці спасылку на падрабязнасьці пра кампанэнт API тут.

Разгледзім прыклад зь цікаючым гадзіньнікам з аднаго з папярэдніх разьдзелаў. У Rendering Elements мы даведаліся толькі пра адзін спосаб абнаўленьня інтэрфэйсу карыстальніка. Мы выклікаем ReactDOM.render(), каб зьмяніць візуалізаваныя выходныя даныя (the rendered output):

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 стварае таймэр ды абнаўляе інтэрфэйс карыстальніка кожную сэкунду, павінен быць дэтальлю рэалізацыі Clock.

У ідэале мы хочам напісаць гэта аднойчы ды мець Clock, які абнаўляе сябе:

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

Каб гэта рэалізаваць, нам трэба дадаць “state” у кампанэнт Clock.

State падобны да props, але ён прыватны ды цалкам кантралюецца кампанэнтам.

Мы згадвалі раней, што кампанэнты, азначаныя як клясы, маюць некаторыя дадатковыя магчымасьці. Менавіта такі лякальны стан (Local state): магчымасьць даступная толькі для клясаў.

Пераўтварэньне функцыі ў кляс

Вы можаце пераўтварыць функцыянальны кампанэнт, кшталту Clock, у кляс за пяць этапаў:

  1. Стварыце з тым жа імем кляс ES6, які пашырае React.Component.

  2. Дадайце да яго адзін пусты мэтад, які называецца render().

  3. Перамясьціце цела функцыі ў мэтад render().

  4. Замяніцеprops на this.props у целе render().

  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, будзе выкарыстоўвацца толькі адзін асобнік клясу Clock. Гэта дазваляе нам выкарыстоўваць дадатковыя магчымасьці, такія як лякальны стан (local state) ды гаплікі жыцьцёвага цыклу (lifecycle hooks).

Дадаваньне лякальнага стану (Local State) да клясу (Class)

Мы перамесьцім date з props у state у тры этапы:

  1. Замяніце this.props.date на this.state.date у мэтадзе render():
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 prop з элемэнта <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. Гэта называецца “мантаж” у React.

Мы таксама хочам ачышчаць гэты таймэр кожны раз, калі вырабляецца DOM з-за таго, што выдаляецца Clock. Гэта называецца “дэмантаж” у React.

Мы можам абвясьціць спэцыяльныя мэтады ў клясе кампанэнта, каб запускаць код, калі кампанэнт манціруецца (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 hooks” (гаплікі жыцьцёвага цыклу).

Гаплік componentDidMount() запускаецца пасьля таго, як выходныя даныя кампанэнта рэндэрацца ў DOM. Гэта добрае месца для стварэньня таймэра:

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

Зьвярніце ўвагу, як мы захавалі ID таймэра якраз на this.

У той час, як this.props ствараюцца самім React, а this.state мае спэцыяльнае значэньне, вы вольныя дадаць дадатковыя палі да кляса ўручную, калі вам трэба захаваць штосьці, што ня ўдзельнічае ў патоку даных (напрыклад, ID таймэра).

Мы будзем разбураць таймэр у гапліку жыцьцёвага цыклу componentWillUnmount():

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

Нарэшце, рэалізуем мэтад, які называецца tick(), і які кампанэнт Clock будзе запускаць кожную сэкунду.

Ён будзе выкарыстоўваць this.setState() для плянаваньня абнаўленьняў у лякальны стан (local 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(), React выклікае канструктар кампанэнта Clock. Так як Clock мусіць паказваць бягучы час, то ён ініцыюе this.state з аб’ектам, які ўключае бягучы час. Мы будзем абнаўляць гэты стан пазьней.

  2. Затым React выклікае мэтад render() кампанэнта Clock. Вось як React пазнае, што павінна быць паказана на экране. Затым React абнаўляе DOM у адпаведнасьці з выходнымі данымі візуалізацыі Clock.

  3. Калі выходныя даныя Clock устаўляюцца ў DOM, React выклікае гаплік жыцьцёвага цыкла componentDidMount(). Унутры яго кампанэнт Clock запытвае браўзэр стварыць таймэр для выкліку мэтаду tick() кампанэнта адзін раз у сэкунду.

  4. Кожную сэкунду браўзэр выклікае мэтад tick(). Унутры яго кампанэнт Clock плянуе абнаўленьне UI выкліканьнем setState() з аб’ектам, які зьмяшчае бягучы час. Дзякуючы выкліку setState(), React ведае, што стан (state) зьмяніўся, і зноў выклікае мэтад render(), каб даведацца, што павінна быць на экране. У гэты час this.state.date у мэтадзе render() будзе адрозным, і таму выходныя даныя візуалізацыі будуць уключаць абноўлены час. Адпаведна React абнаўляе DOM.

  5. Калі кампанэнт Clock наагул выдаляецца з DOM, то React выклікае гаплік жыцьцёвага цыкла componentWillUnmount(), таму таймэр спыняецца.

Правільнае ўжываньне стану (State)

Ёсьць тры рэчы, якія вы павінны ведаць пра setState().

Не зьмяняйце стан (State) непасрэдна

Напрыклад, гэта ня будзе паўторна рэндэрыць кампанэнт:

// Няправільна
this.state.comment = 'Hello';

Замест гэтага выкарыстоўвайце setState():

// Правільна
this.setState({comment: 'Hello'});

Адзінае месца, дзе вы можаце прызначыць this.state, гэта — канструктар.

Абнаўленьні стану (State) могуць быць асынхроннымі

React можа сабраць некалькі выклікаў setState() у адно абнаўленьне дзеля прадукцыйнасьці.

Паколькі this.props ды this.state могуць быць абноўленыя асынхронна, то вы не павінны спадзявацца на іхныя значэньні для вылічэньня наступнага стану (state).

Напрыклад, гэты код можа не абнавіць лічыльнік:

// Няправільна
this.setState({
  counter: this.state.counter + this.props.increment,
});

Каб гэта выправіць, выкарыстоўвайце другую форму setState(), якая прымае функцыю, а не аб’ект. Гэтая функцыя будзе атрымліваць папярэдні state у якасьці першага аргумэнта, а props у момант, калі ўжываецца абнаўленьне, у якасьці другога аргумэнта:

// Правільна
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

Вышэй мы выкарысталі стрэлкавую функцыю, але гэта працуе таксама й са звычайнымі функцыямі:

// Правільна
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});

Абнаўленьні стану (State) аб’ядноўваюцца

Калі вы выклікаеце setState(), React аб’ядноўвае дадзены вамі аб’ект зь цяперашнім 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.

Даныя цякуць уніз

Ні бацькоўскія, ні сыноўнія кампанэнты ня могуць ведаць, ці зьяўляецца які-небудзь кампанэнт stateful (са станам) альбо stateless (бяз стану), і ім ня важна, азначаны ён як функцыя, ці як кляс.

Вось чаму state называюць лякальным (local) ці інкапсуляваным (encapsulated). Ён не даступны для любога кампанэнта, іншага чым той, які валодае й кіруе ім.

Кампанэнт можа выбраць перадаць свой state уніз у якасьці props да сваіх сыноўніх кампанэнтаў:

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

Таксама гэта працуе для вызначаных карыстальнікам кампанэнтаў:

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

Кампанэнт FormattedDate можа атрымаць date у свае props ды ня ведаць, ці ён прыйшоў са стану (state) Clock, альбо з уласьцівасьцяў (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 стварае свой уласны таймэр і абнаўляецца незалежна.

У прыкладных праграмах React, у залежнасьці ад таго, са станам (stateful) кампанэнт, ці бяз стану (stateless), улічваюць дэталь рэалізацыі кампанэнта, які можа зьмяняцца з цягам часу. Вы можаце выкарыстоўваць кампанэнты бяз стану (stateless) унутры кампанэнтаў са станам (stateful), і наадварот.