Реакція на введення зі станом

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

  • Як декларативне програмування інтерфейсу відрізняється від імперативного
  • Як перерахувати різні візуальні стани, у яких може перебувати ваш компонент
  • Як викликати зміни між різними візуальними станами з коду

Як декларативний інтерфейс порівнюється з імперативним

Під час проектування взаємодії з користувацьким інтерфейсом ви, ймовірно, думаєте про те, як інтерфейс змінюється у відповідь на дії користувача. Розглянемо форму, яка дозволяє користувачеві надсилати відповідь:

  • Коли ви вводите щось у форму, кнопка "Надіслати" стає активною
  • При натисканні кнопки "Відправити" форма і кнопка стають неактивними,і з'являється спіннер .
  • Якщо мережевий запит виконано успішно, форму буде приховано, і з'явиться повідомлення "Дякуємо" .
  • У разі невдалого мережевого запиту з'являється повідомлення про помилку , і форма знову стає активною.

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

Вони не знають, куди ви хочете йти, вони просто виконують ваші команди. (І якщо ви неправильно зрозумієте вказівки, то потрапите не туди!). Це називається імперативним, тому що ви маєте "командувати" кожним елементом, від дзиґи до кнопки, вказуючи комп'ютеру як оновити інтерфейс користувача.

У цьому прикладі імперативного UI програмування форма побудована без React. Він використовує лише браузер DOM:

async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() === 'istanbul') {
        resolve();
      } else {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;
{
  "hardReloadOnChange": true
}
<form id="form">
  <h2>City quiz</h2>
  <p>
    What city is located on two continents?
  </p>
  <textarea id="textarea"></textarea>
  <br />
  <button id="button" disabled>Submit</button>
  <p id="loading" style="display: none">Loading...</p>
  <p id="error" style="display: none; color: red;"></p>
</form>
<h1 id="success" style="display: none">That's right!</h1>

<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
</style>

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

React був створений для вирішення цієї проблеми.

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

Думати про інтерфейс користувача декларативно

Вище ви бачили, як реалізувати форму імперативно. Щоб краще зрозуміти, як мислити в React, нижче ми розглянемо повторну реалізацію цього UI в React:

  1. Визначення різних візуальних станів вашого компонента
  2. Визначте що викликає ці зміни стану
  3. Представлення стану в пам'яті за допомогою useState
  4. Видаліть будь-які несуттєві змінні стану
  5. Підключіть обробники подій для встановлення стану

Крок 1: Визначте різні візуальні стани вашого компонента

У комп'ютерних науках ви можете почути про "автомат", який перебуває в одному з декількох "станів". Якщо ви працюєте з дизайнером, ви могли бачити макети для різних "візуальних станів". React знаходиться на перетині дизайну та комп'ютерних наук, тому обидві ці ідеї є джерелом натхнення.

Перш за все, вам потрібно візуалізувати всі різні "стани" інтерфейсу, які може побачити користувач:

  • Порожньо: На формі не працює кнопка "Відправити".
  • Typing: Форма має увімкнену кнопку "Відправити".
  • .
  • Submitting: Форма повністю відключена. Відображено спінер.
  • Успішно: замість форми показано повідомлення "Дякуємо".
  • error: Те саме, що й стан введення, але з додатковим повідомленням про помилку.

Так само, як і дизайнер, ви захочете "макетувати" або створити "макети" для різних станів, перш ніж додавати логіку. Наприклад, ось макет лише візуальної частини форми. Цей макет керується пропсом status зі значенням за замовчуванням 'empty':

export default function Form({
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Submit
        </button>
      </form>
    </>
  )
}

Ви можете назвати цей проп як завгодно, назва не має значення. Спробуйте змінити status = 'empty' на status = 'success', щоб побачити повідомлення про успішне виконання. Імітація дозволяє швидко ітерувати інтерфейс перед тим, як підключати будь-яку логіку. Ось більш деталізований прототип цього ж компонента, все ще "керований" пропом status:

export default function Form({
  // Try 'submitting', 'error', 'success':
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea disabled={
          status === 'submitting'
        } />
        <br />
        <button disabled={
          status === 'empty' ||
          status === 'submitting'
        }>
          Submit
        </button>
        {status === 'error' &&
          <p className="Error">
            Good guess but a wrong answer. Try again!
          </p>
        }
      </form>
      </>
  );
}
.Error { color: red; }

Відображення багатьох візуальних станів одночасно

Якщо компонент має багато візуальних станів, може бути зручно показати їх усі на одній сторінці:

import Form from './Form.js';

let statuses = [
  'empty',
  'typing',
  'submitting',
  'success',
  'error',
];

export default function App() {
  return (
    <>
      {statuses.map(status => (
        <section key={status}>
          <h4>Form ({status}):</h4>
          <Form status={status} />
        </section>
      ))}
    </>
  );
}
export default function Form({ status }) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <form>
      <textarea disabled={
        status === 'submitting'
      } />
      <br />
      <button disabled={
        status === 'empty' ||
        status === 'submitting'
      }>
        Submit
      </button>
      {status === 'error' &&
        <p className="Error">
          Good guess but a wrong answer. Try again!
        </p>
      }
    </form>
  );
}
section { border-bottom: 1px solid #aaa; padding: 20px; }
h4 { color: #222; }
body { margin: 0; }
.Error { color: red; }

Такі сторінки часто називають "живими посібниками зі стилю" або "книгами історій".

Крок 2: Визначте, що викликає ці зміни стану

Ви можете запускати оновлення стану у відповідь на два типи входів:

  • Введення даних людиною, наприклад, натискання кнопки, введення даних у поле, перехід за посиланням.
  • Комп'ютерні вхідні дані, такі як надходження відповіді від мережі, завершення таймауту, завантаження зображення.

В обох випадках, ви повинні встановити змінні стану для оновлення інтерфейсу користувача.Для форми, яку ви розробляєте, вам потрібно буде змінювати стан у відповідь на кілька різних входів:

  • Зміна введення тексту (людина) повинна перевести його зі стану Порожнє у стан Введення або назад, залежно від того, порожнє текстове поле чи ні.
  • Натискання кнопки "Надіслати" (людина) має перевести його у стан "Надсилання" .
  • Успішна відповідь мережі (комп'ютера) повинна перевести його у стан Успішно.
  • Невдала відповідь мережі (комп'ютера) повинна перевести його у стан Помилки з відповідним повідомленням про помилку.

Зверніть увагу, що людські дані часто вимагають обробників подій!

Щоб допомогти візуалізувати цей потік, спробуйте намалювати кожен стан на папері у вигляді позначеного кола, а кожну зміну між двома станами у вигляді стрілки. Таким чином ви можете накреслити багато потоків і відсортувати помилки задовго до реалізації.

Flow chart moving left to right with 5 nodes. The first node labeled 'empty' has one edge labeled 'start typing' connected to a node labeled 'typing'. That node has one edge labeled 'press submit' connected to a node labeled 'submitting', which has two edges. The left edge is labeled 'network error' connecting to a node labeled 'error'. The right edge is labeled 'network success' connecting to a node labeled 'success'.

Сформувати стани

Крок 3: Представлення стану в пам'яті за допомогою useState

Далі вам потрібно буде представити візуальні стани вашого компонента у пам'яті за допомогою useState. Ключовим моментом є простота: кожна частина стану є "рухомою частиною", і вам потрібно якомога менше "рухомих частин". Більша ускладненість призводить до більшої кількості проблем!

Почніть зі стану, який обов'язково повинен бути там. Наприклад, вам потрібно зберігати answer для введення, і error (якщо вона існує) для зберігання останньої помилки:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

Далі вам знадобиться змінна стану, яка представлятиме один із візуальних станів, який ви хочете відобразити. Зазвичай у пам'яті існує більше ніж один спосіб представлення цього стану, тому вам доведеться поекспериментувати з ним.

Якщо вам важко одразу придумати найкращий спосіб, почніть з додавання достатньої кількості станів, щоб ви були точно впевнені, що всі можливі візуальні стани охоплено:

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

Ваша перша ідея, ймовірно, не буде найкращою, але це нормально - стан рефакторингу є частиною процесу!

Крок 4: Видалення несуттєвих змінних стану

Ви хочете уникнути дублювання у вмісті стану, тому відстежуєте лише те, що є важливим. Витративши трохи часу на рефакторинг вашої структури стану, ви зробите ваші компоненти простішими для розуміння, зменшите дублювання та уникнете непередбачуваних значень. Ваша мета полягає у тому, щоб запобігти випадкам, коли стан у пам'яті не представляє жодного коректного інтерфейсу користувача, який ви хотіли б бачити. (Наприклад, ви ніколи не хочете показувати повідомлення про помилку і одночасно вимикати введення, інакше користувач не зможе виправити помилку!)

Ось кілька запитань, які ви можете задати про ваші змінні стану:

  • Чи спричиняє цей стан парадокс? Наприклад, isTyping і isSubmitting не можуть бути одночасно істинними. Парадокс зазвичай означає, що стан недостатньо обмежений. Існує чотири можливі комбінації двох булевих виразів, але лише три з них відповідають допустимим станам. Щоб видалити "неможливий" стан, ви можете об'єднати їх у -статус, який має бути одним з трьох значень: 'typing', 'submitting' або 'success'.
  • Чи є та сама інформація в іншій змінній стану? Ще один парадокс: isEmpty і isTyping не можуть бути істинними водночас. Зробивши їх окремими змінними стану, ви ризикуєте, що вони не будуть синхронізовані і спричинять проблеми. На щастя, ви можете вилучити isEmpty і замість цього перевірити answer.length === 0.
  • .
  • Чи можете ви отримати ту саму інформацію з інверсії іншої змінної стану? isError не потрібно, оскільки ви можете перевірити error !== null замість цього.

Після цього очищення у вас залишиться 3 (з 7!) основних змінних стану:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

Ви знаєте, що вони необхідні, тому що ви не можете видалити жодну з них без порушення функціональності.

Усунення "неможливих" станів за допомогою редуктора

Ці три змінні досить добре відображають стан цієї форми. Однак, є ще деякі проміжні стани, які не мають повного сенсу. Наприклад, ненульова error не має сенсу, коли status дорівнює 'success'. Щоб точніше змоделювати стан, ви можете витягти його у редуктор. Редуктори дозволяють об'єднати декілька змінних стану в один об'єкт і консолідувати всю пов'язану з ними логіку!

Крок 5: Підключення обробників подій для встановлення стану

Нарешті, створіть обробники подій, які оновлюють стан. Нижче наведено фінальну форму з усіма підключеними обробниками подій:

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}
.Error { color: red; }

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

  • Декларативне програмування означає опис інтерфейсу для кожного візуального стану, а не мікрокерування інтерфейсом (імператив).
  • При розробці компонента:
    1. Визначити всі його візуальні стани.
    2. Визначити людські та комп'ютерні тригери зміни стану.
    3. Змоделюйте стан за допомогою useState.
    4. Видалити несуттєві стани, щоб уникнути помилок та парадоксів.
    5. Підключіть обробники подій для встановлення стану.

Додавання та видалення класу CSS

Зробіть так, щоб при натисканні на картинку видалявся CSS клас background--active із зовнішнього <div>, але додавався клас picture--active до <img>. Повторне клацання на фоні має відновити початкові класи CSS.

Візуально, ви повинні очікувати, що натискання на картинку прибирає фіолетовий фон і виділяє межу картинки. Клацання поза межами зображення підсвічує тло, але прибирає підсвічування межі зображення.

export default function Picture() {
  return (
    <div className="background background--active">
      <img
        className="picture"
        alt="Rainbow houses in Kampung Pelangi, Indonesia"
        src="https://i.imgur.com/5qwVYb1.jpeg"
      />
    </div>
  );
}
body { margin: 0; padding: 0; height: 250px; }

.background {
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #eee;
}

.background--active {
  background: #a6b5ff;
}

.picture {
  width: 200px;
  height: 200px;
  border-radius: 10px;
}

.picture--active {
  border: 5px solid #a6b5ff;
}

Цей компонент має два візуальні стани: коли зображення активне та коли зображення неактивне:

  • Коли зображення активне, використовуються класи CSS background та picture picture--active.
  • Коли зображення неактивне, використовуються класи CSS background background--active та picture.

Достатньо однієї булевої змінної стану, щоб запам'ятати, чи активне зображення. Початковим завданням було видалення або додавання класів CSS. Однак у React вам потрібно описати те, що ви хочете бачити, а не маніпулювати елементами інтерфейсу. Отже, вам потрібно обчислити обидва CSS-класи на основі поточного стану. Також потрібно зупинити поширення, щоб клік на зображенні не реєструвався як клік на фоні.

Переконайтеся, що ця версія працює, натиснувши на зображення, а потім поза ним:

import { useState } from 'react';

export default function Picture() {
  const [isActive, setIsActive] = useState(false);

  let backgroundClassName = 'background';
  let pictureClassName = 'picture';
  if (isActive) {
    pictureClassName += ' picture--active';
  } else {
    backgroundClassName += ' background--active';
  }

  return (
    <div
      className={backgroundClassName}
      onClick={() => setIsActive(false)}
    >
      <img
        onClick={e => {
          e.stopPropagation();
          setIsActive(true);
        }}
        className={pictureClassName}
        alt="Rainbow houses in Kampung Pelangi, Indonesia"
        src="https://i.imgur.com/5qwVYb1.jpeg"
      />
    </div>
  );
}
body { margin: 0; padding: 0; height: 250px; }

.background {
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #eee;
}

.background--active {
  background: #a6b5ff;
}

.picture {
  width: 200px;
  height: 200px;
  border-radius: 10px;
  border: 5px solid transparent;
}

.picture--active {
  border: 5px solid #a6b5ff;
}

Або ж ви можете повернути два окремі фрагменти JSX:

import { useState } from 'react';

export default function Picture() {
  const [isActive, setIsActive] = useState(false);
  if (isActive) {
    return (
      <div
        className="background"
        onClick={() => setIsActive(false)}
      >
        <img
          className="picture picture--active"
          alt="Rainbow houses in Kampung Pelangi, Indonesia"
          src="https://i.imgur.com/5qwVYb1.jpeg"
          onClick={e => e.stopPropagation()}
        />
      </div>
    );
  }
  return (
    <div className="background background--active">
      <img
        className="picture"
        alt="Rainbow houses in Kampung Pelangi, Indonesia"
        src="https://i.imgur.com/5qwVYb1.jpeg"
        onClick={() => setIsActive(true)}
      />
    </div>
  );
}
body { margin: 0; padding: 0; height: 250px; }

.background {
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #eee;
}

.background--active {
  background: #a6b5ff;
}

.picture {
  width: 200px;
  height: 200px;
  border-radius: 10px;
  border: 5px solid transparent;
}

.picture--active {
  border: 5px solid #a6b5ff;
}

Майте на увазі, що якщо два різних фрагменти JSX описують одне дерево, їх вкладеність (перший <div> → перший <img>) має бути однаковою. Інакше перемикання isActive відтворить все дерево нижче, а скине його стан. Ось чому, якщо в обох випадках повертається схоже дерево JSX, краще писати їх як один фрагмент JSX.

Редактор профілів

Це невелика форма, реалізована за допомогою звичайного JavaScript та DOM. Пограйтеся з нею, щоб зрозуміти її поведінку:

function handleFormSubmit(e) {
  e.preventDefault();
  if (editButton.textContent === 'Edit Profile') {
    editButton.textContent = 'Save Profile';
    hide(firstNameText);
    hide(lastNameText);
    show(firstNameInput);
    show(lastNameInput);
  } else {
    editButton.textContent = 'Edit Profile';
    hide(firstNameInput);
    hide(lastNameInput);
    show(firstNameText);
    show(lastNameText);
  }
}

function handleFirstNameChange() {
  firstNameText.textContent = firstNameInput.value;
  helloText.textContent = (
    'Hello ' +
    firstNameInput.value + ' ' +
    lastNameInput.value + '!'
  );
}

function handleLastNameChange() {
  lastNameText.textContent = lastNameInput.value;
  helloText.textContent = (
    'Hello ' +
    firstNameInput.value + ' ' +
    lastNameInput.value + '!'
  );
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
{
  "hardReloadOnChange": true
}
<form id="form">
  <label>
    First name:
    <b id="firstNameText">Jane</b>
    <input
      id="firstNameInput"
      value="Jane"
      style="display: none">
  </label>
  <label>
    Last name:
    <b id="lastNameText">Jacobs</b>
    <input
      id="lastNameInput"
      value="Jacobs"
      style="display: none">
  </label>
  <button type="submit" id="editButton">Edit Profile</button>
  <p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>

<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>

Ця форма перемикається між двома режимами: у режимі редагування ви бачите вхідні дані, а у режимі перегляду - лише результат. Підпис кнопки змінюється між "Редагувати" і "Зберегти" залежно від режиму, в якому ви перебуваєте. Коли ви змінюєте вхідні дані, привітальне повідомлення внизу оновлюється у реальному часі.

Ваше завдання - повторно реалізувати його в React у пісочниці нижче. Для вашої зручності розмітка вже була перетворена на JSX, але вам потрібно зробити так, щоб вона показувала і приховувала вхідні дані, як це робить оригінал.

Переконайтеся, що він також оновить текст внизу!

export default function EditProfile() {
  return (
    <form>
      <label>
        First name:{' '}
        <b>Jane</b>
        <input />
      </label>
      <label>
        Last name:{' '}
        <b>Jacobs</b>
        <input />
      </label>
      <button type="submit">
        Edit Profile
      </button>
      <p><i>Hello, Jane Jacobs!</i></p>
    </form>
  );
}
label { display: block; margin-bottom: 20px; }

Вам знадобляться дві змінні стану для зберігання вхідних значень: firstName та lastName. Вам також знадобиться змінна стану isEditing, яка визначатиме, відображати вхідні дані чи ні. Вам слід не вимагати змінну fullName, оскільки повну назву завжди можна обчислити з firstName та lastName.

Нарешті, слід використати умовний рендеринг, щоб показати або приховати входи залежно від isEditing.

import { useState } from 'react';

export default function EditProfile() {
  const [isEditing, setIsEditing] = useState(false);
  const [firstName, setFirstName] = useState('Jane');
  const [lastName, setLastName] = useState('Jacobs');

  return (
    <form onSubmit={e => {
      e.preventDefault();
      setIsEditing(!isEditing);
    }}>
      <label>
        First name:{' '}
        {isEditing ? (
          <input
            value={firstName}
            onChange={e => {
              setFirstName(e.target.value)
            }}
          />
        ) : (
          <b>{firstName}</b>
        )}
      </label>
      <label>
        Last name:{' '}
        {isEditing ? (
          <input
            value={lastName}
            onChange={e => {
              setLastName(e.target.value)
            }}
          />
        ) : (
          <b>{lastName}</b>
        )}
      </label>
      <button type="submit">
        {isEditing ? 'Save' : 'Edit'} Profile
      </button>
      <p><i>Hello, {firstName} {lastName}!</i></p>
    </form>
  );
}
label { display: block; margin-bottom: 20px; }

Порівняйте це рішення з оригінальним імперативним кодом. Чим вони відрізняються?

Рефакторинг імперативного рішення без React

Ось оригінальна пісочниця з попереднього завдання, написана без React:

function handleFormSubmit(e) {
  e.preventDefault();
  if (editButton.textContent === 'Edit Profile') {
    editButton.textContent = 'Save Profile';
    hide(firstNameText);
    hide(lastNameText);
    show(firstNameInput);
    show(lastNameInput);
  } else {
    editButton.textContent = 'Edit Profile';
    hide(firstNameInput);
    hide(lastNameInput);
    show(firstNameText);
    show(lastNameText);
  }
}

function handleFirstNameChange() {
  firstNameText.textContent = firstNameInput.value;
  helloText.textContent = (
    'Hello ' +
    firstNameInput.value + ' ' +
    lastNameInput.value + '!'
  );
}

function handleLastNameChange() {
  lastNameText.textContent = lastNameInput.value;
  helloText.textContent = (
    'Hello ' +
    firstNameInput.value + ' ' +
    lastNameInput.value + '!'
  );
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
{
  "hardReloadOnChange": true
}
<form id="form">
  <label>
    First name:
    <b id="firstNameText">Jane</b>
    <input
      id="firstNameInput"
      value="Jane"
      style="display: none">
  </label>
  <label>
    Last name:
    <b id="lastNameText">Jacobs</b>
    <input
      id="lastNameInput"
      value="Jacobs"
      style="display: none">
  </label>
  <button type="submit" id="editButton">Edit Profile</button>
  <p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>

<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>

Уявіть, що React не існує. Чи можете ви рефакторити цей код так, щоб логіка була менш крихкою і більш схожою на версію React? Як би він виглядав, якби стан був явним, як у React?

Якщо ви не знаєте, з чого почати, наведена нижче заглушка вже містить більшу частину структури. Якщо ви почнете звідси, заповніть пропущену логіку у функції updateDOM. (Якщо потрібно, зверніться до оригінального коду.)

let firstName = 'Jane';
let lastName = 'Jacobs';
let isEditing = false;

function handleFormSubmit(e) {
  e.preventDefault();
  setIsEditing(!isEditing);
}

function handleFirstNameChange(e) {
  setFirstName(e.target.value);
}

function handleLastNameChange(e) {
  setLastName(e.target.value);
}

function setFirstName(value) {
  firstName = value;
  updateDOM();
}

function setLastName(value) {
  lastName = value;
  updateDOM();
}

function setIsEditing(value) {
  isEditing = value;
  updateDOM();
}

function updateDOM() {
  if (isEditing) {
    editButton.textContent = 'Save Profile';
    // TODO: show inputs, hide content
  } else {
    editButton.textContent = 'Edit Profile';
    // TODO: hide inputs, show content
  }
  // TODO: update text labels
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
{
  "hardReloadOnChange": true
}
<form id="form">
  <label>
    First name:
    <b id="firstNameText">Jane</b>
    <input
      id="firstNameInput"
      value="Jane"
      style="display: none">
  </label>
  <label>
    Last name:
    <b id="lastNameText">Jacobs</b>
    <input
      id="lastNameInput"
      value="Jacobs"
      style="display: none">
  </label>
  <button type="submit" id="editButton">Edit Profile</button>
  <p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>

<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>

Відсутня логіка включала перемикання відображення входів та вмісту, а також оновлення міток:

let firstName = 'Jane';
let lastName = 'Jacobs';
let isEditing = false;

function handleFormSubmit(e) {
  e.preventDefault();
  setIsEditing(!isEditing);
}

function handleFirstNameChange(e) {
  setFirstName(e.target.value);
}

function handleLastNameChange(e) {
  setLastName(e.target.value);
}

function setFirstName(value) {
  firstName = value;
  updateDOM();
}

function setLastName(value) {
  lastName = value;
  updateDOM();
}

function setIsEditing(value) {
  isEditing = value;
  updateDOM();
}

function updateDOM() {
  if (isEditing) {
    editButton.textContent = 'Save Profile';
    hide(firstNameText);
    hide(lastNameText);
    show(firstNameInput);
    show(lastNameInput);
  } else {
    editButton.textContent = 'Edit Profile';
    hide(firstNameInput);
    hide(lastNameInput);
    show(firstNameText);
    show(lastNameText);
  }
  firstNameText.textContent = firstName;
  lastNameText.textContent = lastName;
  helloText.textContent = (
    'Hello ' +
    firstName + ' ' +
    lastName + '!'
  );
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
{
  "hardReloadOnChange": true
}
<form id="form">
  <label>
    First name:
    <b id="firstNameText">Jane</b>
    <input
      id="firstNameInput"
      value="Jane"
      style="display: none">
  </label>
  <label>
    Last name:
    <b id="lastNameText">Jacobs</b>
    <input
      id="lastNameInput"
      value="Jacobs"
      style="display: none">
  </label>
  <button type="submit" id="editButton">Edit Profile</button>
  <p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>

<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>

Функція updateDOM, яку ви написали, показує, що React робить за лаштунками, коли ви встановлюєте стан. (Втім, React також уникає задіювання DOM для властивостей, які не змінилися з моменту останнього встановлення)

.