Состояние — JS: React
Вы уже основательно изучили JSX и неинтерактивный способ работы с компонентами React. С этого урока начинается самая главная часть: взаимодействие с пользователем. Ключевые понятия, которые будут рассмотрены в этом уроке: события и состояние. Пример:
Компоненты, которые создавались в курсе раньше, были stateless, то есть не содержали никакого состояния и могли только отрисовывать переданные свойства. Компонент в примере выше является stateful, так как сохраняет внутри себя состояние текущего времени. По порядку:
- Внутри компонента, в конструкторе, определяется начальное состояние, с которым будет инициализирован компонент после отрисовки. Единственное требование к состоянию, которое предъявляет React — тип данных: он должен быть объектом. То, что хранится внутри, определяется самим приложением.
Способ задания начального состояния выглядит так:
class Clock extends React.Component constructor(props) super(props); // всегда обязательно this.state = date: new Date() >; > >
Обратите внимание на то, что это единственное место, где state может изменяться напрямую (точнее, создаваться). Во всех остальных местах this.state должен использоваться только для чтения! Подробнее об этом дальше.
- Функция render использует данные из state для отрисовки. Здесь никаких сюрпризов.
- На кнопку вешается обработчик на клик. В отличие от HTML, в свойство onClick передается функция и она вызовется автоматически в момент срабатывания события. Внутри обработчика определяется текущая дата и идет установка нового состояния. Ещё раз: крайне важно не изменять state напрямую. Для установки нового состояния в React предусмотрена функция setState . Именно её вызов приводит к тому, что компонент, в конце концов, перерисуется. Происходит это не сразу, то есть setState работает асинхронно и внутренняя магия пытается оптимизировать процесс рисования.
Ещё один важный момент заключается в том, как определена функция handleClick . Так как происходит работа с классом, то логично было бы использовать такой стиль определения:
class Clock extends React.Component handleClick() this.setState( date: new Date() >); > >
Но такой подход плохо работает в React по двум причинам.
Первая заключается в том, что обработчики вызываются асинхронно, а методы в классах — это обычные функции с поздним связыванием. Поэтому нельзя просто так повесить обработчик, так как он потеряет this . С таким определением придется постоянно писать подобный код: onClick= либо такой onClick= this.handleClick()> .
Вторая причина связана с производительностью. Оба предыдущих примера передачи обработчика порождают при каждом вызове функции render новые обработчики (так как функции сравниваются по ссылкам, а не по содержимому), а для React это критично. Поэтому правильный способ определения — стрелочная функция:
class Clock extends React.Component handleClick = () => this.setState( date: new Date() >); > >
Ещё один пример:
See the Pen by Hexlet (@hexlet) on CodePen.
Логично ожидать, что счётчик будет увеличиваться на 2 при каждом клике, но этого не происходит. Как уже говорилось выше, this.setState выполняется Реактом не сразу. Следовательно, может возникнуть ситуация, когда state или props изменились к тому моменту, когда будет выполняться изменение состояния. Для таких случаев, когда новое состояние определяется на основе предыдущего (или на основе пропсов) у setState предусмотрена возможность принимать функцию вместо объекта:
this.setState((state, props) => const count > = state; return count: count + 1 >; >)
Попробуйте переписать пример со счётчиком, используя описанную форму setState .
По большому счёту, описанные выше механизмы открывают практически все двери. Теперь вы с лёгкостью можете создавать интерактивные компоненты и оживлять ваш UI. Всё остальное — это тонкости, предусмотренные для различных ситуаций.
Инициализация
Представьте, что в компоненте, созданном выше, нужно инициализировать счётчик со свойством count , переданным снаружи. И только в его отсутствие ставить 0. Для решения этой задачи нужно добавить две вещи:
- Использовать свойство count как начальное значение счетчика.
- Добавить значение по умолчанию для свойства count .
setState
В следующем примере реализованы две кнопки, каждая из которых управляет своим состоянием.
В данном примере объект состояния включает два свойства: count для одной кнопки и primary для другой. Основная хитрость этого примера заключается в процессе обновления состояния:
// первая кнопка this.setState(( count >) => ( count: count + 1 >)); // вторая кнопка this.setState(( primary >) => ( primary: !primary >));
Функция setState заменяет значения ключей в предыдущем состоянии на значения этих же ключей в новом состоянии. То, что внутри функции не возвращалось, не трогается. Фактически происходит слияние старого состояния и нового. На практике это поведение крайне удобно, иначе пришлось бы каждый раз выполнять работу по слиянию руками.
Структура объекта состояния
Существует множество способов организации данных внутри состояния. Скорее всего, вы захотите хранить их как-то так:
const blogPosts = [ id : "post1", author : username : "user1", name : "User 1">, body : ". ", comments : [ id : "comment1", author : username : "user2", name : "User 2">, comment : ". ", >, id : "comment2", author : username : "user3", name : "User 3">, comment : ". ", > ] >, id : "post2", author : username : "user2", name : "User 2">, body : ". ", comments : [ id : "comment3", author : username : "user3", name : "User 3">, comment : ". ", >, ] > // and repeat many times ]
При таком подходе сущности, зависимые от других, находятся внутри. Если брать пример выше, то это означает, что каждый пост содержит внутри себя как автора, так и список комментариев, а каждый комментарий, в свою очередь, содержит внутри свои связанные сущности того же автора. При таком подходе получается, что состояние представляет собой дерево зависимостей. Хотя этот способ организации кажется вполне естественным, работать с ним крайне тяжело. Во-первых, одни и те же данные начнут дублироваться в разных местах и вам придётся синхронизировать изменения в них, что создает космические проблемы на пустом месте. Во-вторых, обновления таких данных (особенно в неизменяемом стиле) становятся сложными и многословными. В-третьих, так как все состояние — это один большой кусок, то любое обновление приведет к его полному копированию, что может быть дорогой операцией (в зависимости от размера состояния и количества обновлений в единицу времени).
Общая рекомендация, которую дают разработчики React, это делать структуру максимально плоской, похожей на то, как хранятся данные в базе данных. Причём желательно в хорошо нормализованном виде. Другими словами, не нужно дублировать данные в состоянии. Пример того как правильно это делать:
const state = articles: [/*. */],
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Состояние. Управление компонентами-классами
Объект state описывает внутреннее состояние компонента, он похож на props за тем исключением, что состояние определяется внутри компонента и доступно только из компонента.
Если props представляет входные данные, которые передаются в компонент извне, то состояние хранит такие объекты, которые создаются в компоненте и полностью зависят от компонента.
Также в отличие от props значения в state можно изменять.
И еще важный момент — значения из state должны использоваться при рендеринге. Если какой-то объект не используется в рендерниге компонента, то нет смысла сохранять его в state.
Нередко state описывает какие-то визуальные свойства элемента, которые могут изменяться при взаимодействие с пользователем. Например, кнопку нажали, и соответственно можно изменить ее состояние — придать ей какой-то другой цвет, тень и так далее. Кнопку нажали повторно — можно вернуть исходное состояние.
Стоит отметить, что традиционно объект state применялся только в классах-компонентах. В функциональных же компонентах для управления состоянием применяется другая архитектура, основанная на хуках.
При использовании класса-компонента единственное место, где можно установить объект state — это конструктор класса:
Hello React
При определении конструктора компонента в нем должен вызываться конструктор базового класса, в который передается объект props .

Обновление состояния
Для обновления состояния вызывается функция setState() :
this.setState();
Изменение состояния вызовет повторный рендеринг компонента, в соответствии с чем веб-страница будет обновлена.
В то же время не стоит изменять свойства состояния напрямую, например:
this.state.welcome = "Привет React";
В данном случае изменения повторного рендеринга компонента происходить не будет.
При этом нам не обязательно обновлять все его значения. В процессе работы программы мы можем обновить только некоторые свойства. Тогда необновленные свойства будут сохранять старые значения.
Пример обновления состояния:
Hello React button < width: 100px; height:30px; border-radius: 4px; margin:50px; >.on < color:#666; background-color: #ccc; >.off
Здесь определен компонент ClickButton, который по сути представляет кнопку. В состоянии кнопки хранится два свойства — надпись и класс. При нажатии на кнопку мы будем переключать с одного класса на другой. Событие нажатия кнопки через атрибут onClick связано с методом press() , в котором переключается класс кнопки.
При этом свойство state.label остается неизменным.

Асинхронное обновление
При наличии нескольких вызовов setState() React может объединять их в один общий пакет обновлений для увеличения производительности.
Так как объекты this.props и this.state могут обновляться асинхронно, не стоит полагаться на значения этих объектов для вычисления состояния. Например:
this.setState(< counter: this.state.counter + this.props.increment, >);
Для обновления надо использовать другую версию функции setState() , которая в качестве параметра принимает функцию. Данная функция имеет два параметра: предыдущее состояние объекта state и объект props на момент применения обновления:
this.setState(function(prevState, props) < return < counter: prevState.counter + props.increment >; >);
Например, определим два последовательных вызова setState() :
METANIT.COM
В props определено свойство increment — значение, на которое будет увеличиваться свойство counter в state ( this.setState(); ). При чем при нажатии кнопки мы предполагаем, что функция setState() будет вызываться два раза, соответственно значение state.counter при нажатии кнопки должно увеличиваться на 2. Однако в реальности увеличение происходит лишь на 1:

Теперь изменим код, применив второй вариант функции setState() :
METANIT.COM
Чтобы избежать повторения, все действия по инкременту вынесены в отдельную функцию — incrementCounter, однако опять же функция setState() вызывается два раза. И теперь инкремент будет срабатывать два раза при однократном нажатии, собственно как и определено в коде и как и должно быть:
props vs state
С тех пор как мы начали использовать React для перестройки наших UI на uberVU (сейчас Hootsuite) вопросом разработчика №1 вероятно был:
В чем же все-таки разница между props и state ?
Разницу довольно просто понять если рассматривать их в контексте, но концептуально понять это будет все-таки сложно.
Контекст
Основной задачей Компонента является перевод необработанных данных в полноценный HTML-код. С учетом этого, props и state вместе представляют необработанные входные данные в HTML.
Можно сказать, что props + state это входные данные для функции Компонента render() , поэтому чтобы понять откуда и что берется нам необходимо рассматриваемые одновременно и другие элементы.
Посколько мы используем Cosmos , где props может содержать первоначально значение ( initial state ), рассмотрение этого вопроса является очень важным.
Точки пересечения
Перед тем, как мы начнем разделять props и state , давайте определим где они взаимодействуют друг с другом.
- props и state обычные объекты JS.
- props и state инициируют выполнение функции render .
- props и state являются предопределенными. Если с одним и тем же надобром props и state в Компоненте вы получаете различные выходные данные, то что-то сделано неправильно.
Does this go inside props or state?
Если Компонент должен изменить один из своих атрибутов в какой-то момент времени, этот атрибут должен быть частью state этого Компонента, в противном случае он должен быть просто prop для него.
props
props (сокр. от properties) является конфигурацией Компонента, или настройками (options) если сказать другими словами.
Компонент не может изменять свой props , но он может отвечать за изменение props своих дочерних Компонентов.
state
state появляется с установки его значения по умолчанию в момент «монтирования» компонента и потом допускает свое изменение со временем (в основном от пользовательских событий). Это серриализуемое* представление в состояния компонента в определенный момент времени.
Компонент сам управляет своим собственным состоянием изнутри, но, помимо установки первоначального состояния, не управляет состоянием своих дочерних Компонентов.
Можно сказать, что state является приватным параметром.
* Мы не говорим, что props тоже сериализуемы, потому что передавать функции обратного вызова через props является довольно частой практикой.
Состояние и жизненный цикл
На этой странице представлены понятия «состояние» (state) и «жизненный цикл» (lifecycle) React-компонентов. Подробный справочник API компонентов находится по этой ссылке.
В качестве примера рассмотрим идущие часы из предыдущего раздела. В главе Рендеринг элементов мы научились обновлять UI только одним способом — вызовом root.render() :
const root = ReactDOM.createRoot(document.getElementById('root')); function tick() const element = ( div> h1>Привет, мир!h1> h2>Сейчас new Date().toLocaleTimeString()>.h2> div> ); root.render(element);> setInterval(tick, 1000);
В этой главе мы узнаем, как инкапсулировать и обеспечить многократное использование компонента Clock . Компонент самостоятельно установит свой собственный таймер и будет обновляться раз в секунду.
Для начала, извлечём компонент, показывающий время:
const root = ReactDOM.createRoot(document.getElementById('root')); function Clock(props) return ( div> h1>Привет, мир!h1> h2>Сейчас props.date.toLocaleTimeString()>.h2> div> ); > function tick() root.render(Clock date=new Date()> />);> setInterval(tick, 1000);
Проблема в том, что компонент Clock не обновляет себя каждую секунду автоматически. Хотелось бы спрятать логику, управляющую таймером, внутри самого компонента Clock .
В идеале мы бы хотели реализовать Clock таким образом, чтобы компонент сам себя обновлял:
root.render(Clock />);
Для этого добавим так называемое «состояние» (state) в компонент Clock .
«Состояние» очень похоже на уже знакомые нам пропсы, отличие в том, что состояние контролируется и доступно только конкретному компоненту.
Преобразование функционального компонента в классовый
Давайте преобразуем функциональный компонент Clock в классовый компонент за 5 шагов:
- Создаём ES6-класс с таким же именем, указываем React.Component в качестве родительского класса
- Добавим в класс пустой метод render()
- Перенесём тело функции в метод render()
- Заменим props на this.props в теле render()
- Удалим оставшееся пустое объявление функции
class Clock extends React.Component render() return ( div> h1>Привет, мир!h1> h2>Сейчас this.props.date.toLocaleTimeString()>.h2> div> ); > >
Теперь Clock определён как класс, а не функция.
Метод render будет вызываться каждый раз, когда происходит обновление. Так как мы рендерим в один и тот же DOM-контейнер, мы используем единственный экземпляр класса Clock — поэтому мы можем задействовать внутреннее состояние и методы жизненного цикла.
Добавим внутреннее состояние в класс
Переместим date из пропсов в состояние в три этапа:
- Заменим this.props.date на this.state.date в методе render() :
class Clock extends React.Component render() return ( div> h1>Привет, мир!h1> h2>Сейчас this.state.date.toLocaleTimeString()>.h2> div> ); > >
- Добавим конструктор класса, в котором укажем начальное состояние в переменной this.state :
class Clock extends React.Component constructor(props) super(props); this.state = date: new Date()>; > render() return ( div> h1>Привет, мир!h1> h2>Сейчас this.state.date.toLocaleTimeString()>.h2> div> ); > >
Обратите внимание, что мы передаём props базовому (родительскому) конструктору:
constructor(props) super(props); this.state = date: new Date()>; >
Классовые компоненты всегда должны вызывать базовый конструктор с аргументом props .
- Удалим проп date из элемента :
root.render(Clock />);
Позже мы вернём код таймера обратно и на этот раз поместим его в сам компонент.
Результат выглядит следующим образом:
class Clock extends React.Component constructor(props) super(props); this.state = date: new Date()>; > render() return ( div> h1>Привет, мир!h1> h2>Сейчас this.state.date.toLocaleTimeString()>.h2> div> ); > > const root = ReactDOM.createRoot(document.getElementById('root')); root.render(Clock />);
Теперь осталось только установить собственный таймер внутри Clock и обновлять компонент каждую секунду.
Добавим методы жизненного цикла в класс
В приложениях со множеством компонентов очень важно освобождать используемые системные ресурсы, когда компоненты удаляются.
Первоначальный рендеринг компонента в DOM называется «монтирование» (mounting). Нам нужно устанавливать таймер всякий раз, когда это происходит.
Каждый раз когда DOM-узел, созданный компонентом, удаляется, происходит «размонтирование» (unmounting). Чтобы избежать утечки ресурсов, мы будем сбрасывать таймер при каждом «размонтировании».
Объявим специальные методы, которые компонент будет вызывать при монтировании и размонтировании:
class Clock extends React.Component constructor(props) super(props); this.state = date: new Date()>; > componentDidMount() > componentWillUnmount() > render() return ( div> h1>Привет, мир!h1> h2>Сейчас this.state.date.toLocaleTimeString()>.h2> div> ); > >
Эти методы называются «методами жизненного цикла» (lifecycle methods).
Метод componentDidMount() запускается после того, как компонент отрендерился в DOM — здесь мы и установим таймер:
componentDidMount() this.timerID = setInterval( () => this.tick(), 1000 ); >
Обратите внимание, что мы сохраняем ID таймера в this ( this.timerID ).
Поля this.props и this.state в классах — особенные, и их устанавливает сам React. Вы можете вручную добавить новые поля, если компоненту нужно хранить дополнительную информацию (например, ID таймера).
Теперь нам осталось сбросить таймер в методе жизненного цикла componentWillUnmount() :
componentWillUnmount() clearInterval(this.timerID); >
Наконец, реализуем метод tick() . Он запускается таймером каждую секунду и вызывает this.setState() .
this.setState() планирует обновление внутреннего состояния компонента:
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>Привет, мир!h1> h2>Сейчас this.state.date.toLocaleTimeString()>.h2> div> ); > > const root = ReactDOM.createRoot(document.getElementById('root')); root.render(Clock />);
Теперь часы обновляются каждую секунду.
Давайте рассмотрим наше решение и разберём порядок, в котором вызываются методы:
- Когда мы передаём в root.render() , React вызывает конструктор компонента. Clock должен отображать текущее время, поэтому мы задаём начальное состояние this.state объектом с текущим временем. Позже мы обновим это состояние.
- React вызывает метод render() компонента Clock . Таким образом React узнаёт, что отобразить на экране. Далее React обновляет DOM так, чтобы он соответствовал выводу рендера Clock .
- Как только вывод рендера Clock вставлен в DOM, React вызывает метод жизненного цикла componentDidMount() . Внутри него компонент Clock указывает браузеру установить таймер, который будет вызывать tick() раз в секунду.
- Таймер вызывает tick() ежесекундно. Внутри tick() мы просим React обновить состояние компонента, вызывая setState() с текущим временем. React реагирует на изменение состояния и снова запускает render() . На этот раз this.state.date в методе render() содержит новое значение, поэтому React заменит DOM. Таким образом компонент Clock каждую секунду обновляет UI.
- Если компонент Clock когда-либо удалится из DOM, React вызовет метод жизненного цикла componentWillUnmount() и сбросит таймер.
Как правильно использовать состояние
Важно знать три детали о правильном применении setState() .
Не изменяйте состояние напрямую
В следующем примере повторного рендера не происходит:
// Неправильно this.state.comment = 'Привет';
Вместо этого используйте setState() :
// Правильно this.setState(comment: 'Привет'>);
Конструктор — это единственное место, где вы можете присвоить значение this.state напрямую.
Обновления состояния могут быть асинхронными
React может сгруппировать несколько вызовов setState() в одно обновление для улучшения производительности.
Поскольку this.props и this.state могут обновляться асинхронно, вы не должны полагаться на их текущее значение для вычисления следующего состояния.
Например, следующий код может не обновить счётчик:
// Неправильно this.setState( counter: this.state.counter + this.props.increment, >);
Правильно будет использовать второй вариант вызова setState() , который принимает функцию, а не объект. Эта функция получит предыдущее состояние в качестве первого аргумента и значения пропсов непосредственно во время обновления в качестве второго аргумента:
// Правильно this.setState((state, props) => ( counter: state.counter + props.increment >));
В данном примере мы использовали стрелочную функцию, но можно использовать и обычные функции:
// Правильно this.setState(function(state, props) return counter: state.counter + props.increment >; >);
Обновления состояния объединяются
Когда мы вызываем setState() , React объединит аргумент (новое состояние) c текущим состоянием.
Например, состояние может состоять из нескольких независимых полей:
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() оставляет this.state.posts нетронутым, но полностью заменяет this.state.comments .
Однонаправленный поток данных
В иерархии компонентов ни родительский, ни дочерние компоненты не знают, задано ли состояние другого компонента. Также не важно, как был создан определённый компонент — с помощью функции или с помощью класса.
Состояние часто называют «локальным», «внутренним» или инкапсулированным. Оно доступно только для самого компонента и скрыто от других.
Компонент может передать своё состояние вниз по дереву в виде пропсов дочерних компонентов:
FormattedDate date=this.state.date> />
Компонент FormattedDate получает date через пропсы, но он не знает, откуда они взялись изначально — из состояния Clock , пропсов Clock или просто JavaScript-выражения:
function FormattedDate(props) return h2>Сейчас props.date.toLocaleTimeString()>.h2>; >
Это, в общем, называется «нисходящим» («top-down») или «однонаправленным» («unidirectional») потоком данных. Состояние всегда принадлежит определённому компоненту, а любые производные этого состояния могут влиять только на компоненты, находящиеся «ниже» в дереве компонентов.
Если представить иерархию компонентов как водопад пропсов, то состояние каждого компонента похоже на дополнительный источник, который сливается с водопадом в произвольной точке, но также течёт вниз.
Чтобы показать, что все компоненты действительно изолированы, создадим компонент App , который рендерит три компонента :
function App() return ( div> Clock /> Clock /> Clock /> div> ); >
У каждого компонента Clock есть собственное состояние таймера, которое обновляется независимо от других компонентов.
В React-приложениях, имеет ли компонент состояние или нет — это внутренняя деталь реализации компонента, которая может меняться со временем. Можно использовать компоненты без состояния в компонентах с состоянием, и наоборот.