Перейти до основного вмісту

Внутрішній стан компонента

Об'єкт-стану state – це властивість класу, яка не повинна безпосередньо змінюватися розробником.

  • Дані в state контролюють те, що відображається в інтерфейсі.
  • Дані, що зберігаються у стані, повинні бути інформацією, яка буде оновлюватися методами компонента.
  • Не потрібно дублювати дані з props у стані.
  • Щоразу, коли змінюється стан компонента (або пропси), викликається метод render().

У стані зберігають мінімально необхідний набір даних, на підставі яких можна обчислити все необхідне для рендеру інтерфейсу. Це робиться викликом селекторів (функцій, які складають дані для інтерфейсу на підставі стану) у методі render(). Таким чином ми отримуємо дані, що обчислюються.

reactivity
  • Інтерфейс залежить від стану компонента.
  • Стан може змінитися як реакція на дії користувача.
  • Під час зміни стану дані передаються вниз по дереву компонентів.
  • Компоненти повертають оновлену розмітку і змінюється інтерфейс.

Стан належить компоненту і змінюється тільки його методами. Зміна стану компонента ніколи не вплине на його батька, сусідів або будь-який інший компонент у застосунку – тільки на його дочірні елементи. За такої моделі дані у застосунку передаються тільки одним, жорстко обмеженим чином. Це називається односпрямований потік даних.

data-flow

Стан оголошується в конструкторі, оскільки це перше, що відбувається, коли створюється екземпляр класу.

class Counter extends Component {
constructor() {
super();

this.state = {
value: 0,
};
}

/* ... */

render() {
return (
<div>
<span>{this.state.value}</span>
{/* ... */}
</div>
);
}
}

Початковий стан від props

Іноді початковий стан залежить від переданих пропсів, наприклад, початкове значення нашого лічильника. У цьому разі необхідно явно оголосити параметр props у конструкторі і передати його у виклик super(props). Тоді в конструкторі буде доступно this.props.

class Counter extends Component {
static defaultProps = {
step: 1,
initialValue: 0,
};

constructor(props) {
super(props);

this.state = {
value: this.props.initialValue,
};
}

/* ... */
}

ReactDOM.render(<Counter initialValue={10} />, document.getElementById("root"));

Оскільки під капотом використовується Babel, можна пропустити стомлююче оголошення конструктора і вказати стан як публічну властивість класу, все інше транспайлер зробить за нас.

class Counter extends Component {
static defaultProps = {
step: 1,
initialValue: 0,
};

state = {
value: this.props.initialValue,
};

/* ... */
}

Зміна стану компонента

Для оновлення стану використовується вбудований метод setState().

setState(updater, callback)
  • Першим, обов'язковим аргументом, передається об'єкт з полями, які вказують, яку частину стану необхідно змінити.
  • Другим, необов'язковим аргументом, можна передати callback-функцію, яка виконається після зміни стану.
небезпека

Не можна змінювати стан безпосередньо за посиланням. Будьте дуже уважні, особливо під час роботи з посилальними типами (масив, об'єкт).

state = { fullName: "Poly" };

// ❌ Погано - зміна за посиланням
this.state.fullName = "Mango";

// ✅ Добре
this.setState({
fullName: "Mango",
});

Цей підхід використовується, коли новий стан не розраховується на підставі попереднього. Тобто, коли у стан записується щось нове, перезаписуючи вже існуюче. Зробимо компонент з перемикачем, методи якого перезаписуватимуть значення isOpen у стані.

class Toggle extends Component {
state = { isOpen: false };

show = () => this.setState({ isOpen: true });

hide = () => this.setState({ isOpen: false });

render() {
const { isOpen } = this.state;
const { children } = this.props;

return (
<>
<button onClick={this.show}>Show</button>
<button onClick={this.hide}>Hide</button>
{isOpen && children}
</>
);
}
}

Як оновлюється стан

Під час виклику setState() не потрібно передавати всі властивості, що зберігаються у стані. Достатньо вказати лише ту частину (зріз) стану, яку ми хочемо змінити у цій операції. React потім бере поточний стан і об'єкт, який був переданий у setState(), об'єднуючи їх наступним чином.

// стан до об'єднання
const currentState = { a: 2, b: 3, c: 7, d: 9 };

// об'єкт, переданий в setState
const updateSlice = { b: 5, d: 4 };

// нове значення this.state після об'єднання
const nextState = { ...currentState, ...updateSlice }; // {a: 2, b: 5, c: 7, d: 4}

Асинхронність оновлення стану

Метод setState() реєструє асинхронну операцію оновлення стану, яка ставиться в чергу оновлень. React змінює стан не для кожного виклику setState(), а може об'єднувати кілька викликів в одне оновлення для підвищення продуктивності. Внаслідок цього, доступ до this.state у синхронному коді після виклику цього методу поверне значення до оновлення.

Уявіть, що при зміні стану ви покладаєтеся на поточне значення стану в обчисленні наступного. Використовуємо цикл for для створення (реєстрації) кількох оновлень.

// Припустимо, що є такий стан
state = { value: 0 };

// Запустимо цикл і створимо 3 операції оновлення
for (let i = 0; i < 3; i += 1) {
// Якщо переглянути стан, на всіх ітераціях буде 0
// Тому що це синхронний код та оновлення стану ще не відбулося
console.log(this.state.value);

this.setState({ value: this.state.value + 1 });
}

Значення властивості this.state.value запам'ятовується під час створення об'єкта, що передається в setState(), а не під час оновлення стану. Тобто, якщо в момент створення об'єкта this.state.value містило 0, у функцію setState() передається об'єкт {value: 0 + 1}.

В результаті виконання циклу отримуємо чергу з 3-х об'єктів {value: 0 + 1}, {value: 0 + 1}, {value: 0 + 1} та оригінальний стан на момент оновлення {value: 0}}. Після всіх оновлень отримуємо стан {value: 1}.

Тому не можна покладатися на поточний стан в обчисленні наступного, що залежить від попереднього на момент оновлення. Це може призвести до помилок. Тому існує другий спосіб оновити стан.

setState з функцією

Цей підхід використовується, коли нове значення обчислюється на підставі попереднього стану. Метод setState() першим аргументом може приймати не об'єкт, а функцію, яка повинна повертати об'єкт, яким ми хочемо оновити стан.

setState((state, props) => {
return {};
}, callback);

Актуальний стан і пропси на момент асинхронного виконання функції, переданої в setState(), будуть передані в неї аргументами state і props. Таким чином, можна бути впевненими у коректному значенні попереднього стану під час створення наступного.

// Припустимо, що є такий стан
state = { value: 0 };

// Запустимо цикл і створимо 3 операції оновлення
for (let i = 0; i < 3; i += 1) {
// Якщо переглянути стан, на всіх ітераціях буде 0
// Тому що це синхронний код та оновлення стану ще не відбулося
console.log(this.state.value); // 0

this.setState(prevState => {
// Якщо переглянути стан, переданий callback-функції під час її виклику,
// отримаємо актуальний стан на момент оновлення.
console.log(prevState.value); // буде різний на кожній ітерації

return { value: prevState.value + 1 };
});
}

Щоразу під час виклику функції, переданої в setState(), в параметр prevState буде передане посилання на актуальний стан в момент оновлення. Отримаємо об'єкти оновлень {value: 0 + 1}, {value: 1 + 1}, {value: 2 + 1}, і, в результаті, this.state.value буде містити 3 .

Тепер можемо замінити функціонал відкрити/закрити у компоненті <Toggle>.

class Toggle extends Component {
state = { isOpen: false };

toggle = () => {
this.setState(state => ({ isOpen: !state.isOpen }));
};

render() {
const { isOpen } = this.state;
const { children } = this.props;

return (
<div>
<button onClick={this.toggle}>{isOpen ? "Hide" : "Show"}</button>
{isOpen && children}
</div>
);
}
}

А лічильник виглядатиме так.

class Counter extends Component {
/* ... */

handleIncrement = () => {
this.setState((state, props) => ({
value: state.value + props.step,
}));
};

handleDecrement = () => {
this.setState((state, props) => ({
value: state.value - props.step,
}));
};

/* ... */
}

Підіймання стану

Оскільки React використовує односпрямований потік даних зверху вниз, для того щоб змінити стан батька під час події в дитині, використовується наступний патерн з callback-функцією.

lifting state
  • У батька є стан і метод, який його змінює.
  • Дочірньому елементу у формі пропу передається метод батька, що змінює стан батька.
  • У дочірньому елементі відбувається виклик переданого йому методу. – Під час виклику цього методу змінюється стан батька.
  • Відбувається рендер піддерева компонентів батька.

Розглянемо простий, але наочний приклад.

// Button отримує функцію changeMessage (ім'я пропа),
// яка викликається під час події onClick
const Button = ({ changeMessage, label }) => (
<button type="button" onClick={changeMessage}>
{label}
</button>
);

class App extends Component {
state = {
message: new Date().toLocaleTimeString(),
};

// Метод, який будемо передавати в Button для виклику під час кліку
updateMessage = evt => {
console.log(evt); // Доступний об'єкт події
this.setState({
message: new Date().toLocaleTimeString(),
});
};

render() {
return (
<>
<span>{this.state.message}</span>
<Button label="Change message" changeMessage={this.updateMessage} />
</>
);
}
}

Під час кліку на кнопці стан App оновлюється за допомогою callback-функції, контекст якої прив'язаний до App. Цей патерн встановлює чітку межу між "розумними" та "дурними" компонентами.

Патерн підіймання стану може мати будь-яку вкладеність.

lifting state

Типи внутрішніх даних компонента-класу

  • static data – статичні властивості і методи, до яких необхідно отримувати доступ без екземпляра.
  • this.state.data – динамічні дані, що змінюються методами компонента, стан.
  • this.data – дані, які будуть різні для кожного екземпляра.
  • const DATA – константи, дані, які не змінюються, та однакові для всіх екземплярів.