Работа с input

Сперва приберемся:

Удалим лишние console.log'и, удалим обработчик handleCounter и параграф, который выводил количество кликов.

Затем создадим компонент - <TestInput />, который будет просто отрисовывать (render) input перед списком новостей.

...
// --- добавили test input ---
class TestInput extends React.Component {
  render() {
    return (
      <input className='test-input' value='введите значение' />
    )
  }
}

const App = () => {
  return (
    <React.Fragment>
      <h3>Новости</h3>
      <TestInput /> {/* добавили вывод компонента */}
      <News data={myNews}/>
    </React.Fragment>
  )
}
...

Напомню про комментарии:

Первый комментарий, добавлен с помощью //, так как данный комментарий не находится внутри JSX. А второй - находится, следовательно имеет вид {/* комментарий */}. То есть, JSX - это не весь код вашего сценария, а только те части, где мы миксуем верстку и js (обычно в return методе у render'a).

Вообще, код сейчас не работает (но это не из-за комментария). Давайте посмотрим на ошибку внимательно:

Вы предоставили свойство value для поля, у которого нет onChange обработчика. Поэтому отрисовано поле только для чтения. Если поле должно быть изменяемо, используйте defaultValue. Либо установите onChange или readOnly. Проверьте render метод компонента TestInput.

Не могу не любить react за такие подробные сообщения об ошибках.

А вы кстати попробуйте сейчас изменить значение инпута. Ничего не выйдет. Здесь у нас есть два пути, и первый нам известный - использовать какое-нибудь свойство state в качестве динамически изменяемого значения инпута.

Controlled components (контролируемые компоненты)

Для вызова setState, будем использовать событие onChange. Работа с ним не отличается от работы с onClick или другими любыми событиями. Главное - передать функцию-обработчик.

Не торопитесь, давайте подумаем еще раз:

  1. Нам нужно передать функцию обработчик, которая будет изменять какую-то переменную состояния (с помощью setState, разумеется).

  2. Значит нам нужно создать начальное состояние (state).

  3. Если у нас есть переменная состояния компонента, значит мы хотим, чтобы именно она была в качестве value у нашего инпута.

Сможете сделать сами? Если да - отлично, если нет - решение ниже.

Подсказка #1: так может выглядеть состояние + функция-обработчик

...
state = { myValue: '' }
...
onChangeHandler = (e) => {
  this.setState({ myValue: e.target.value })
},
...

Решение:

class TestInput extends React.Component {
  state = {
    myValue: '',
  }
  // используется e.currentTarget.value
  onChangeHandler = (e) => {
    this.setState({ myValue: e.currentTarget.value })
  }
  render() {
    return (
      <input
        className='test-input'
        onChange={this.onChangeHandler}
        value={this.state.myValue} 
        placeholder='введите значение' />
    )
  }
}

У нас есть placeholder - "введите значение", который будет показываться в момент загрузки страницы, так как наше начальное состояние input'a - пустая строка. При изменении, мы устанавливаем в переменную myValue - то что введено в input. Следовательно - input корректно изменяется.

e.currentTarget vs e.target

Документация, что за свойство currentTarget (MDN)

Представьте ситуацию: у вас будет onClick стоять на div, внутри которого будет текст в параграфе. В итоге, при клике на текст в паграфе:

  • e.target будет равно параграфу (и это верно, по спецификации)

  • e.currentTarget будет равно div'у (и это то, что нам обычно и нужно)

Кто вообще не понял о чем идет речь - нужно читать основы про объект события или весь раздел основы работы с событиями целиком (Кантор).

Обычно, мы хотим по клику отправлять значения инпута...

Задача: По клику на кнопку - показывать alert с текстом инпута.

Попробуйте сами.

Подсказка #1:

Вам необходимо:

  • добавить кнопку в <TestInput />;

  • на кнопку "повесить" обработчик onClick;

  • в функции обработчике считывать значение this.state.myValue;

Подсказка #2:

Так как нам необходимо рендерить больше одного элемента, нужно обернуть их в родительский элемент, например в <div></div> или React.Fragment

Решение:

class TestInput extends React.Component {
  state = {
    myValue: '',
  }
  onChangeHandler = (e) => {
    this.setState({ myValue: e.currentTarget.value })
  }
  onBtnClickHandler = (e) => {
    alert(this.state.myValue);
  }
  render() {
    return (
      <React.Fragment>
        <input
          className='test-input'
          onChange={this.onChangeHandler}
          value={this.state.myValue}
          placeholder='введите значение' />
        <button onClick={this.onBtnClickHandler}>Показать alert</button>
      </React.Fragment>
    )
  }
}

Предлагаю добавить отступы для .test-input:

css/app.css

...
.test-input {
  margin: 0 5px 5px 0;
}
...

После добавления отступа в данном коде ничего не раздражает. Или нет? Как думаете, что здесь может расстроить борца за оптимизацию?

Ответ: каждый раз, после любого изменения у нас вызывается setState, а значит - полная перерисовка компонента. Не очень приятно. Опять же, чуть больше логики в момент render'a компонента и в пору будет расстроиться от "отзывчивого" поля ввода.

У нас логики в render-методе никакой нет, поэтому для нас это лишняя оптимизация. Однако, рассмотреть способ создания "неконтролируемых компонентов" нужно обязательно.

Документация по react, предлагает вам использовать в большинстве случаев контролируемые компоненты.

Uncontrolled Components (неконтролируемый компонент)

Главное отличие неконтролируемого компонента от контролируемого в том, что у него нет обработчика изменений, а значит нет постоянных вызовов setState и перерисовок.

Для того чтобы считать значение неконтролируемого компонента используется механизм refs.

Для неконтролируемого компонента в момент начальной загрузки можно указывать defaultValue.

Начнем по порядку:

  1. Удалим обработчик onChange

  2. Удалим state

  3. Укажем defaultValue = пустая строка (defaultValue='') вместо value

  4. В constructor методе компонента (оп-па) укажем this.input = React.createRef();

  5. Добавим атрибут ref инпуту, равный this.input (который в конструкторе создали)

class TestInput extends React.Component {
  constructor(props) {
    super(props)
    this.input = React.createRef()
  }
  onBtnClickHandler = (e) => { // эта запись сейчас не работает
    alert(this.state.myValue);
  }
  render() {
    return (
      <React.Fragment>
          <input
            className='test-input'
            defaultValue=''
            placeholder='введите значение'
            ref={this.input}
          />
        <button onClick={this.onBtnClickHandler}>Показать alert</button>
      </React.Fragment>
    )
  }
}

Обновите страницу, попробуйте ввести значение. Работает? Работает!

Теперь нам нужно, научиться считывать значение: перепишите onBtnClickHandler следующим образом:

onBtnClickHandler = () => {
  alert(this.input.current.value)
},

В документации так же есть пример использования refs и доступа к файлу, загружаемому через <input type="file" />.

За этот урок, мы научились с вами не вызывать дорогой setState и render на "каждый чих".

P.S. конечно, в данном случае никакого выигрыша в производительности нет. Оба подхода хорошо сработают.

Вариант с контролируемыми и неконтролируемыми компонентами, работа с defaultValue и state являются одинаковыми для всех элементов форм.

Очень рекомендую посмотреть страницу документации на англ.языке по элементам форм

Исходный код на данный момент (включая alert и console.log). Пока что оставлен input с ref.

Last updated