React setState: правильный путь
В этом руководстве я объясню, как изменить состояние в компонентах React с помощью встроенного метода setState
. Я подробно расскажу о двух разных подходах к использованию этого метода, объясню различия между этими подходами и покажу, когда использовать какой. В конце я расскажу о типичных ошибках, которые могут возникнуть при изменении состояния.
TL; DR
- Используйте функцию
this.setState
, чтобы изменить состояние ваших компонентов - Если вы не зависите от старого состояния, передайте методу
this.setState
объект с новыми значениями - Если вы хотите изменить состояние в зависимости от предыдущих значений, передайте функцию методу
this.setState
- Передаваемая функция принимает предыдущее состояние и пропсы в качестве аргументов.
- Если вы работаете с объектами из своего состояния, всегда копируйте их, чтобы избежать непреднамеренных изменений
Все настраивается
Состояние - это просто данные, которые вы хотите сохранить в своем компоненте. В этом руководстве я собираюсь использовать компонент на основе классов со следующей начальной настройкой:
class App extends Component {
state = {
counter: 0,
};
clickHandler = () => {};
render() {
return (
<React.Fragment>
<div>{this.state.counter}</div>
<button onClick={this.clickHandler}>Click!</button>
</React.Fragment>
);
}
}
У меня есть <div>
и кнопка. Для метода класса я использую стрелочную функцию, внутри которой я вызываю this.setState
, потому что я не хочу терять свой this
. Если вы хотите использовать function
, ваш код должен быть следующим:
class App extends Component {
constructor(props) {
this.clickHandler = this.clickHandler.bind(this);
}
state = {
counter: 0,
};
clickHandler = function () {};
render() {
return (
<React.Fragment>
<div>{this.state.counter}</div>
<button onClick={this.clickHandler}>Click!</button>
</React.Fragment>
);
}
}
или, немного короче:
class App extends Component {
constructor(props) {
this.clickHandler = this.clickHandler.bind(this);
}
state = {
counter: 0,
};
clickHandler() {}
render() {
return (
<React.Fragment>
<div>{this.state.counter}</div>
<button onClick={this.clickHandler}>Click!</button>
</React.Fragment>
);
}
}
В приведенном выше коде я использую метод .bind
для настройки функции с привязанным к ней значением this
. Но в следующих примерах я буду придерживаться синтаксиса стрелочной функции, поскольку он более точен и элегантен.
Обновление состояния: основы
Попробуем сгенерировать случайное число и показать его в нашем <div>
при нажатии кнопки.
Прежде всего следует упомянуть, что вы не можете напрямую обновлять состояние вашего компонента. Я имею в виду, что это не сработает:
class App extends Component {
state = {
counter: 0,
};
clickHandler = () => {
this.state.counter = Math.random();
};
render() {
return (
<React.Fragment>
<div>{this.state.counter}</div>
<button onClick={this.clickHandler}>Click!</button>
</React.Fragment>
);
}
}
Кроме того, вы получите раздражающее предупреждение в консоли, если используете инструменты разработчика React. Вместо того, чтобы напрямую изменять ваше состояние, вам нужно использовать функцию this.setState
:
clickHandler = () => {
this.setState({ counter: Math.random() });
};
Эта функция является функцией, унаследованной вами от класса React.Component
, и является частью React API. Он принимает новое состояние вашего компонента. Здесь важно упомянуть, что вам не нужно повторять все свойства, которые есть в вашем состоянии, чтобы изменить только одно значение. Эти два объекта будут объединены функцией this.setState
. Рассмотрим следующий пример:
import React, { Component } from 'react';
export default class App extends Component {
state = {
counter: 0,
title: 'setState tutorial',
};
clickHandler = () => {
this.setState({ counter: Math.random() });
};
render() {
return (
<React.Fragment>
<h1>{this.state.title}</h1>
<div>{this.state.counter}</div>
<button onClick={this.clickHandler}>Click!</button>
</React.Fragment>
);
}
}
В приведенном выше коде я показываю заголовок в теге <h1>
, который я храню внутри состояния. Когда кнопка нажата, заголовок не исчезнет.
Обновление состояния в зависимости от предыдущего состояния
Все идет нормально. Но что, если мы хотим увеличить наш предыдущий счетчик при нажатии кнопки? Технически следующий код будет работать так, как задумано:
import React, { Component } from 'react';
export default class App extends Component {
state = {
counter: 0,
title: 'setState tutorial',
};
clickHandler = () => {
this.setState({ counter: ++this.state.counter });
};
render() {
return (
<React.Fragment>
<h1>{this.state.title}</h1>
<div>{this.state.counter}</div>
<button onClick={this.clickHandler}>Click!</button>
</React.Fragment>
);
}
}
Единственное, что вы могли заметить (если вы последуете по тексту, я настоятельно рекомендую вам это сделать), - это раздражающее предупреждение в консоли. На самом деле, когда мы вызываем ++this.state.counter
, мы пытаемся изменить счетчик в нашем предыдущем состоянии и вернуть значение математической операции. Не так хорошо, как хотелось бы. К счастью, React умеет делать подобное. Метод this.setState
также принимает функцию, которая затем должна вернуть новое состояние. Кроме того, функция принимает два аргумента: предыдущее состояние и свойства:
import React, { Component } from 'react';
export default class App extends Component {
state = {
counter: 0,
title: 'setState tutorial',
};
clickHandler = () => {
this.setState((prevState, prevProps) => {
console.log(prevState); // {counter: 0, title: "setState tutorial"}
return { counter: ++prevState.counter };
});
};
render() {
return (
<React.Fragment>
<h1>{this.state.title}</h1>
<div>{this.state.counter}</div>
<button onClick={this.clickHandler}>Click!</button>
</React.Fragment>
);
}
}
Теперь он работает без каких-либо предупреждений. Давайте обсудим, что произошло.
Зачем мне использовать функцию?
Возможно, вы говорите: я могу скопировать значение счетчика и затем увеличить его. Как в примере ниже:
let oldCounter = this.state.counter;
this.setState({ counter: ++oldCounter });
Поскольку числа в javascript являются примитивами, в консоли не будет никаких предупреждений. Но это все еще неправильный путь. Дело в том, что React обновляет состояние асинхронно, даже если метод this.setState
вызывается синхронно. Поэтому, когда вы обновляете свое состояние таким образом, вы не можете с уверенностью сказать, что это последняя версия состояния. Здесь это не проблема, так как у нас крошечное приложение, и все обновления происходят почти сразу, но в более крупных приложениях это может вызвать проблемы. Итак, используйте функцию, если вы хотите обновить свое состояние в зависимости от предыдущих значений.
А как насчет объектов?
Ладно, теперь все хорошо, думаете вы. Вы защищены this.setState((prevState, prevProps) => ...)
. Однако есть еще один подводный камень. В javascript объекты хранятся по ссылкам. Давайте быстро проверим нашу функцию clickHandler
:
clickHandler = () => {
this.setState((prevState, prevProps) => {
console.log(prevState === this.state); // true
return { counter: ++prevState.counter };
});
};
Баам! Объект prevState
является тем же this.state
. Когда мы имеем дело с примитивами, это не проблема. Но рассмотрим следующий пример:
// Counter.js
import React from 'react';
const Counter = ({ counter, clickHandler }) => {
return (
<React.Fragment>
<div>{counter}</div>
<button onClick={clickHandler}>Increment the counter above!</button>
</React.Fragment>
);
};
export default Counter;
// App.js
import React, { Component } from 'react';
import Counter from './Counter';
export default class App extends Component {
state = {
counters: [0, 0],
};
clickHandler = index => {
this.setState((prevState, prevProps) => {
let counter = prevState.counters[index];
const counters = prevState.counters;
counters[index] = ++counter;
return {
counters,
};
});
};
render() {
const counters = this.state.counters.map((counter, index) => (
<Counter
counter={counter}
key={index}
clickHandler={() => this.clickHandler(index)}
/>
));
return <React.Fragment>{counters}</React.Fragment>;
}
}
В приведенном выше примере у меня есть два счетчика и дополнительный компонент, который отображает в <div>
текущий счетчик и кнопку для его увеличения. Когда вы нажимаете кнопку, вы видите, что счетчик увеличивается в два раза. Не то, что мы искали.
Итак, в чем проблема?
В этих строках:
const counters = prevState.counters;
counters[index] = ++counter;
Здесь мы работаем с тем же массивом counters
, что и в состоянии out. А массивы - это объекты. Следовательно, мы увеличиваем счетчик в два раза. Чтобы исправить это, нам просто нужно скопировать этот массив:
clickHandler = index => {
this.setState((prevState, prevProps) => {
let counter = prevState.counters[index];
const counters = [...prevState.counters];
counters[index] = ++counter;
return {
counters,
};
});
};
Поэтому, работая с объектами, не забывайте копировать их перед выполнением каких-либо операций.
Вывод
- Используйте функцию
this.setState
, чтобы изменить состояние ваших компонентов - Если вы не зависите от старого состояния, передайте методу
this.setState
объект с новыми значениями - Если вы хотите изменить состояние в зависимости от предыдущих значений, передайте функцию методу
this.setState
- Передаваемая функция принимает предыдущее состояние и пропсы в качестве аргументов.
- Если вы работаете с объектами из своего состояния, всегда копируйте их, чтобы избежать непреднамеренных изменений