State и Lifecycle

Тази страница представя концепцията за state(състояние) и lifecycle(жизнен цикъл) на компонентите в React. Можете да намерите подробна информация за API(програмния интерфейс) на компонент тук.

Спомнете си за примера с часовника в един от предишните раздели. В Рендериране на елементи, до сега научихме само един начин да актуализираме потребителския интерфейс. Извикваме 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 създава таймер и актуализира потребителският интерфейс всяка секунда, но това трябва да бъде имплементирано в самия Clock.

В идеалния случай ние бихме искали да напишем това само веднъж и Clock да се актуализира сам:

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

За да имплементираме това, ще трябва да добавим state към компонента Clock.

State e подобен на props, но е вътрешен за компонента и се контролира напълно от самия него.

Както споменахме в предишния раздел компонентите, които са дефинирани като класове имат допълнителни свойства. Локалния 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 ще бъде използвана. Това ни позволява да използваме допълнителни свойства като локален state и “lifecycle методи”.

Добавяне на локален State към Клас

Ще преместим 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. Добавяме в класа constructor метод, който ще присвои 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 метод:

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

Компонентите които са дефинирани като класове трябва винаги да извикват базовия constructor с 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 дървото за първи път. Това се нарича “mounting”(закачане) в React.

Също така искаме да зачистваме таймера всеки път, когато Clock бъде премахнат от DOM дървото. Това се нарича “unmounting”(разкачане) в React.

Можем да декларираме специални методи в компонент клас, за да изпълним определен код, когато компонента се закача или разкача:

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
    );
  }

Забележете че запазваме ID-то на таймера директно в this (this.timerID).

Докато this.props се дефинират от самия React, this.state има специална употреба. Имаме възможността да добавяме допълнителни полета към класа ръчно ако имаме нужда да съхраняваме неща, които не участват в потока от данни (например ID-то на таймера).

Ще зачистим таймера в 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(), React извиква конструктор метода на компонента Clock. Тъй като Clock трябва да визуализира текущото време, той инициализира this.state с обект съдържащ текущото време. В по-късен етап ще актуализираме this.state.

  2. React извиква метода render() на компонента Clock. По този начин React разбира, какво трябва да бъде визуализирано на екрана. След това React актуализира DOM дървото, така че да съответства на резултата от рендерирането на Clock.

  3. Когато резултата на Clock бъде добавен към DOM дървото, React извиква lifecycle метода componentDidMount(). Вътре в него, компонента Clock казва на browser-а да настройва таймера да извиква метода tick() на всяка секунда.

  4. Всяка секунда browser-а извиква метода tick(). Вътре в него, компонента Clock насрочва промяна в потребителският интерфейс като извиква метода setState() с обект съдържащ текущото време. Когато setState() бъде извикан, React разбира че state-а е променен, и извиква render() метода отново за да разбере какво трябва да се визуализира на екрана. Този път, this.state.date намиращ се в render() метода ще бъде различен, и така резултата от render метода ще съдържа актуализираното време. Съответно React ще актуализира DOM дървото.

  5. Ако компонента Clock бъде изтрит от DOM дървото, React ще извика lifecycle метода componentWillUnmount() и така таймерът ще спре.

Как да използваме правилно State

Има три неща които трябва да знаете относно setState().

State-а не трябва да бъде променян директно

Следния пример няма да рендерира компонента наново:

// Wrong
this.state.comment = 'Hello';

Вместо това използвайте setState():

// Correct
this.setState({comment: 'Hello'});

Единственото място в което this.state може да бъде присвоен е в конструктора.

Актуализирането на State-а може да бъде асинхронно

React може да групира множество извиквания на setState() в една единствена актуализация, за да подобри производителността.

Тъй като this.props и this.state може да бъдат актуализирани асинхронно, не трябва да разчитате на текущите им стойности, когато пресмятате бъдещият state.

Следният примерен код, може да не успее да актуализира таймера коректно:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

За да поправите това, използвайте друга форма на setState(), която приема като аргумент функция вместо обект. Тази функция ще получи предишният state като първи аргумент и props като втори аргумент в момента в който е имало актуализация.

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

В горния пример използваме arrow функция, но също така може да бъде използвана и обикновенна такава:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.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-а е наричан локален или капсулиран. Той не е достъпен от никой друг компонент освен този, в който е дефиниран и променян.

Компонентът може да подаде своят state като prop към компонентите, които са му деца.

<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 е част от един специфичен компонент и всякакви данни или потребителски интерфейс получени като резултат от 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” компоненти и обратното.