Учебный курс по React, часть 17: пятый этап работы над TODO-приложением, модификация состояния компонентов

Автор оригинала: Bob Ziroll
  • Перевод
  • Tutorial
В сегодняшней части перевода курса по React мы предлагаем вам выполнить очередное практическое задание и представляем вашему вниманию рассказ о том, как модифицировать состояние компонентов React.

image

Часть 1: обзор курса, причины популярности React, ReactDOM и JSX
Часть 2: функциональные компоненты
Часть 3: файлы компонентов, структура проектов
Часть 4: родительские и дочерние компоненты
Часть 5: начало работы над TODO-приложением, основы стилизации
Часть 6: о некоторых особенностях курса, JSX и JavaScript
Часть 7: встроенные стили
Часть 8: продолжение работы над TODO-приложением, знакомство со свойствами компонентов
Часть 9: свойства компонентов
Часть 10: практикум по работе со свойствами компонентов и стилизации
Часть 11: динамическое формирование разметки и метод массивов map
Часть 12: практикум, третий этап работы над TODO-приложением
Часть 13: компоненты, основанные на классах
Часть 14: практикум по компонентам, основанным на классах, состояние компонентов
Часть 15: практикумы по работе с состоянием компонентов
Часть 16: четвёртый этап работы над TODO-приложением, обработка событий
Часть 17: пятый этап работы над TODO-приложением, модификация состояния компонентов
Часть 18: шестой этап работы над TODO-приложением
Часть 19: методы жизненного цикла компонентов
Часть 20: первое занятие по условному рендерингу
Часть 21: второе занятие и практикум по условному рендерингу
Часть 22: седьмой этап работы над TODO-приложением, загрузка данных из внешних источников
Часть 23: первое занятие по работе с формами
Часть 24: второе занятие по работе с формами

Занятие 31. Практикум. TODO-приложение. Этап №5


Оригинал

▍Задание


Запуская наше Todo-приложение, вы могли заметить, что в консоль выводится уведомление, которое указывает на то, что мы, настроив свойство checked элемента в компоненте TodoItem, не предусмотрели механизм для взаимодействия с этим элементом в виде обработчика события onChange. При работе с интерфейсом приложения это выражается в том, что флажки, выводимые на странице, нельзя устанавливать и снимать.

Здесь вам предлагается оснастить элемент типа checkbox компонента TodoItem обработчиком события onChange, который, на данном этапе работы, достаточно представить в виде функции, которая что-то выводит в консоль.

▍Решение


Вот как сейчас выглядит код компонента TodoItem, который хранится в файле TodoItem.js:

import React from "react"

function TodoItem(props) {
    return (
        <div className="todo-item">
            <input type="checkbox" checked={props.item.completed}/>
            <p>{props.item.text}</p>
        </div>
    )
}

export default TodoItem

Вот что выводится в консоль при запуске приложения.


Уведомление в консоли

При этом флажки на наши воздействия не реагируют.

Для того чтобы избавиться от этого уведомления и подготовить проект к дальнейшей работе, достаточно назначить обработчик события onChange элементу checkbox. Вот как это выглядит в коде:

import React from "react"

function TodoItem(props) {
    return (
        <div className="todo-item">
            <input 
                type="checkbox" 
                checked={props.item.completed} 
                onChange={() => console.log("Changed!")}
            />
            <p>{props.item.text}</p>
        </div>
    )
}

export default TodoItem

Здесь мы, в качестве обработчика, используем простую функцию, которая выводит в консоль слово Checked!. При этом щелчки по флажкам не приводят к изменению их состояния, но уведомление из консоли, как можно видеть на следующем рисунке, исчезает.


Флажки всё ещё не работают, но уведомление из консоли исчезло

Это небольшое изменение, внесённое в приложение, позволит нам, после того, как мы разберёмся с изменением состояния компонентов, сделать так, чтобы флажки работали правильно.

Занятие 32. Изменение состояния компонентов


Оригинал

Начнём работу со стандартного приложения, создаваемого с помощью create-react-app, в файле App.js которого содержится такой код:

import React from "react"

class App extends React.Component {
    constructor() {
        super()
        this.state = {
            count: 0
        }
    }
    
    render() {
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button>Change!</button>
            </div>
        )
    }
}

export default App

В файле стилей index.css, который подключён в файле index.js, содержится следующее описание стилей:

div {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

h1 {
    font-size: 3em;
}

button {
    border: 1px solid lightgray;
    background-color: transparent;
    padding: 10px;
    border-radius: 4px;   
}

button:hover {
    cursor: pointer;
}

button:focus {
    outline:0;
}

На данном этапе работы приложение выглядит так, как показано на следующем рисунке.


Страница приложения в браузере

Сегодня мы будем говорить о том, как менять состояние компонентов. Если у компонента есть состояние, это позволяет, инициализировав его, хранить в нём какие-то данные. Но если бы состояние нельзя было бы изменять, то от его наличия у компонента не было бы особенной пользы, хранение в нём данных не особенно сильно отличалось бы от, например, жёсткого задания их в коде компонента.

Поговорим о приложении, на примере которого мы будем рассматривать работу с состоянием компонента. Компонент App, код которого представлен выше, представляет собой компонент, основанный на классе. Это вполне очевидно, так как нам нужно, чтобы у этого компонента было бы состояние. В коде компонента мы используем конструктор.

В нём мы, как всегда, вызываем метод super() и инициализируем состояние, записывая в него свойство count и присваивая ему начальное значение 0. В методе render() мы выводим заголовок первого уровня, представляющий значение свойства count из состояния компонента, а также кнопку со словом Change!. Всё это отформатировано с помощью стилей.

Если, на данном этапе работы над приложением, открыть его в браузере и щёлкнуть по кнопке, то ничего, естественно, не произойдёт. Нам же нужно чтобы щелчок по кнопке менял бы состояние компонента, воздействуя на его свойство count. При этом мы уже изучили методику обработки событий в React, и наша задача сводится к тому, чтобы создать механизм, который, реагируя на щелчок по кнопке, меняет свойство состояния count.

Приступим к решению нашей задачи, оснастив кнопку обработчиком события onClick, который, для начала, будет просто выводить что-нибудь в консоль.

Для этого мы добавим в класс компонента новый метод. Назвать его можно как угодно, но подобные методы принято называть так, чтобы их имена указывали бы на обрабатываемые ими события. В результате мы, так как мы собираемся с его помощью обрабатывать событие click, назовём его handleClick(). Вот как теперь будет выглядеть код компонента App.

import React from "react"

class App extends React.Component {
    constructor() {
        super()
        this.state = {
            count: 0
        }
    }
    
    handleClick() {
        console.log("I'm working!")
    }
    
    render() {
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button onClick={this.handleClick}>Change!</button>
            </div>
        )
    }
}

export default App

Обратите внимание на то, что обращаясь к этому методу из render(), мы используем конструкцию вида this.handleClick.

Теперь, если щёлкнуть по кнопке, в консоль попадёт соответствующее сообщение.


Щелчок по кнопке вызывает метод класса

Сейчас давайте сделаем так, чтобы щелчок по кнопке увеличивал бы число, выводимое над ней, то есть, модифицировал бы состояние компонента. Может быть, попробовать поменять состояние компонента напрямую, в методе handleClick()? Скажем, что если переписать этот метод так:

handleClick() {
    this.state.count++
}

Нужно сразу сказать, что так с состоянием компонентов в React не работают. Попытка выполнения подобного кода вызовет ошибку.

Состояние компонента можно сравнить с одеждой, которую носит человек. Если он хочет переодеться, то он не перешивает и не перекрашивает одежду, не снимая с себя, а снимает её и надевает что-то другое. Собственно говоря, именно так работают и с состоянием компонентов.

Возможно, вы помните о том, что мы говорили о специальном методе, используемом для модификации состояния, доступном в компонентах, основанных на классах благодаря тому, что они расширяют класс React.Component. Это — метод setState(). Его используют в тех случаях, когда нужно изменить состояние компонента. Этим методом можно пользоваться по-разному.

Вспомним о том, что состояние представляет собой объект. Попробуем передать методу setState() объект, который заменит состояние. Перепишем метод handleClick() так:

handleClick() {
    this.setState({ count: 1 })
}

Попытка воспользоваться таким методом вызовет такую ошибку: TypeError: Cannot read property 'setState' of undefined. На самом деле, то о чём мы сейчас говорим, вызывает множество споров в среде React-разработчиков, и сейчас я собираюсь показать вам очень простой способ решения этой проблемы, который, на первый взгляд, может показаться необычным.

Речь идёт о том, что каждый раз, создавая метод класса (handleClick() в нашем случае), в котором планируется использовать метод setState(), этот метод нужно связать с this. Делается это в конструкторе. Код компонента после этой модификации будет выглядеть так:

import React from "react"

class App extends React.Component {
    constructor() {
        super()
        this.state = {
            count: 0
        }
        this.handleClick = this.handleClick.bind(this)
    }
    
    handleClick() {
        this.setState({ count: 1 })
    }
    
    render() {
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button onClick={this.handleClick}>Change!</button>
            </div>
        )
    }
}

export default App

Теперь после нажатия на кнопку Change! над ней появится число 1, сообщений об ошибках выводиться не будет.


Нажатие на кнопку модифицирует состояние

Правда, кнопка у нас получилась «одноразовой». После первого щелчка по ней 0 меняется на 1, а если щёлкнуть по ней ещё раз — ничего уже не произойдёт. В общем-то, это и неудивительно. Код, вызываемый при щелчке по кнопке, делает своё дело, каждый раз меняя состояние на новое, правда, после первого же щелчка по кнопке новое состояние, в котором в свойстве count хранится число 1, не будет отличаться от старого. Для того чтобы решить эту проблему, рассмотрим ещё один способ работы с методом setState().

Если нас не интересует то, каким было предыдущее состояние компонента, то этому методу можно просто передать объект, который заменит состояние. Но часто бывает так, что новое состояние компонента зависит от старого. В нашем случае это означает, что мы, опираясь на значение свойства count, которое хранится в предыдущей версии состояния, хотим прибавить к этому значению 1. В случаях, когда для изменения состояния нужно быть в курсе того, что в нём хранилось ранее, методу setState() можно передать функцию, которая, в качестве параметра, получает предыдущую версию состояния. Назвать этот параметр можно как угодно, в нашем случае это будет prevState. Заготовка этой функции будет выглядеть так:

handleClick() {
    this.setState(prevState => {
            
    })
}

Можно подумать, что в подобной функции достаточно просто обратиться к состоянию с помощью конструкции вида this.state, но такой подход нас не устроит. Поэтому важно, чтобы эта функция принимала бы предыдущую версию состояния компонента.

Функция должна возвращать новую версию состояния. Вот как будет выглядеть метод handleClick(), решающий эту задачу:

handleClick() {
    this.setState(prevState => {
        return {
            count: prevState.count + 1
        }
    })
}

Обратите внимание на то, что для получения нового значения свойства count мы используем конструкцию count: prevState.count + 1. Можно подумать, что тут подойдёт и конструкция вида count: prevState.count++, но оператор ++ приводит к модификации переменной, к которой он применяется, это будет означать попытку модификации предыдущей версии состояния, поэтому им мы здесь не пользуемся.

Полный код файла компонента на данном этапе работы будет выглядеть так:

import React from "react"

class App extends React.Component {
    constructor() {
        super()
        this.state = {
            count: 0
        }
        this.handleClick = this.handleClick.bind(this)
    }
    
    handleClick() {
        this.setState(prevState => {
            return {
                count: prevState.count + 1
            }
        })
    }
    
    
    render() {
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button onClick={this.handleClick}>Change!</button>
            </div>
        )
    }
}

export default App

Теперь каждый щелчок по кнопке увеличивает значение счётчика.


Каждый щелчок по кнопке увеличивает значение счётчика

То, с чем мы только что разобрались, открывает для нас огромные возможности в сфере разработки React-приложений.

Ранее мы говорили о том, что родительский компонент может, через механизм свойств, передавать свойства из собственного состояния дочерним компонентам. Если React обнаружит изменение состояния родительского компонента, он выполнит повторный рендеринг дочернего компонента, которому передаётся это состояние. Выглядит это как вызов метода render(). В результате дочерний компонент будет отражать новые данные, хранящиеся в состоянии родительского компонента.

Итоги


Сегодня вы подготовили Todo-приложение к дальнейшей работе над ним, а также ознакомились с механизмами, используемыми в React для изменения состояния компонента. В следующий раз вам будет предложено расширить возможности учебного приложения с использованием того, что вы узнали сегодня.

Уважаемые читатели! Как вы относитесь к тому, что состояние компонентов в React нельзя менять напрямую, без использования специальных механизмов?

RUVDS.com
881,00
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Комментарии 11

    0
    Автор, большое спасибо, давай еще! Использование спец механизмов для изменения состояния как я понимаю — вынужденная необходимость, иначе бы пришлось наследовать классы состояний от какого-то Observable и все стало бы еще сложнее
      0
      Спасибо большое за ваши труды. Очень хороший курс. Ждем следующий выпуск)
        0
        Огромное спасибо за перевод.
        Там небольшая опечатка в методе handleClick(). return без буквы r написан:)
          0
          исправили
          0
          Объясните, пожалуйста, дураку почему обязательно использовать prevState?
          И чем
          this.setState(prevState => {
                      return {
                          count: prevState.count + 1
                      }
                  })

          Лучше
          this.setState({ count: this.state.count + 1 });

          ?
            0
            Из документации React:
            this.setState является асинхронной функцией и поэтому несколько вызовов этой функции в одном цикле может привести к объединению этих вызовов. Например, если мы захотим увеличить количество товара на 1 больше одного раза за цикл, то получим следующий результат:
            Object.assign(
            previousState,
            {quantity: state.quantity + 1},
            {quantity: state.quantity + 1},

            )
            Последующий вызов this.setState в одном цикле будет перезаписывать предыдущий, поэтому количество товара будет увеличено только один раз. Если новое состояние зависит от предыдущего, то мы рекомендуем использовать вариант setState с вызовом функции:
            this.setState((state) => {
            return {quantity: state.quantity + 1};
            });
            Документация
            0
            Использовать конструкцию вида
            constructor() {
              super();
              ...
              this.handleClick = this.handleClick.bind(this)
            }
            

            означает создавать в каждом экземпляре класса одноименное свойство handleClick, а, следовательно, значит игнорировать аналогичный метод в прототипе.
            Мне кажется это антипаттерн.

            Есть ли среди читателей сего курса знатоки которые смогут парировать моё утверждение или порекомендовать лучшую практику для решения задачи сохранения контекста?

              0
              Думаю лучше передавать контекст через замыкание:

              render() {
                      return (
                          <div>
                              <h1>{this.state.count}</h1>
                              <button onClick={()=>this.handleClick()}>Change!</button>
                          </div>
                      )
              }
              
                0
                Такой подход ещё хуже.
                Смотри jsx-no-lambda tslint правило.
                Суть в том что вы объявляете колбэк на каждой итерации render, что отрицательно сказывается на производительности.
                  0
                  Спасибо за правило, буду знать. А как такой вариант?
                      constructor() {
                          super();
                          this.state = {
                              count: 0
                          };
                      }
                  
                      handleClick = () =>{
                          this.setState(prevState => {
                              return {
                                  count: prevState.count + 1
                              }
                          })
                      };
                  
                  
                      render() {
                          return (
                              <div>
                                  <h1>{this.state.count}</h1>
                                  <button onClick={this.handleClick}>Changed!</button>
                              </div>
                          )
                      }
                  

                  Подсмотрел отсюда stackoverflow
                    0
                    Этот вариант очень похож на вариант с .bind. Здесь метод тоже прописывается в экземпляре класса, а не в прототипе.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое