Связываем компоненты вместе

На данный момент у нас имеются все компоненты, которые нам нужны, пришло время объединять их.

В этом уроке мы должны ответить на несколько вопросов:

  • Где хранить список?

  • Каким образом мы добавляем новые пункты?

  • Как отобразить несколько дел сразу?

  • Каким образом мы их удаляем?

В этом уроке присутствует повышенная концентрация JavaScript. Я постараюсь объяснить все сложные моменты, а глубже с теорией мы познакомимся в следующей главе.

Где хранить список?

У нас есть два компонента: первый (Field.jsx) добавляет новые элементы, второй (Item.jsx) отображает их и имеет возможность удалить. Так как компоненты не должны знать друг о друге, быть максимально изолированными, хранить данные мы будем в ближайшем общем предке, стало быть, TodoApp.jsx:

TodoApp.jsx
export function TodoApp() {
  const [items, setItems] = useState([]);

...

Каким образом мы добавляем новые пункты?

Нам нужен способ сообщить в главный компонент (TodoApp), что произошло нажатие кнопки добавления, и что Field хочет добавить новую запись в наш список. В Реакте это делается с помощью коллбеков. Коллбек (от англ. call back — "перезвони мне") — это функция, которая передается из родительского компонента в дочерний как параметр, которую дочерний компонент вызывает для того, чтобы сигнализировать родителю о том, что произошло какое-то действие. Давай посмотрим, как мы можем применить это в нашей ситуации.

Для начала добавим Field в TodoApp(не забудь импортировать его):

TodoApp.jsx
import { Field } from './field';

export function TodoApp() {
  //...

  return (
    <div>
      <Field />
    </div>
  );
}

Создадим функцию, которая будет коллбеком и добавим ее к пропсам Field:

TodoApp.jsx
export function TodoApp() {
  const [items, setItems] = useState([]);


  function addNewItem() {

  }

  return (
    <div>
      <Field onAdd={addNewItem} />
    </div>
  );
}

Перейдем в Field.jsx и пропишем этот коллбек как обработчик нажатия кнопки:

Field.jsx
export function Field(props) { // не забудь добавить сюда props

  const [text, setText] = useState('');

  //...
  
  function onButtonClick() {
    props.onAdd(text)
  }

  return (
    //...
    <button onClick={onButtonClick}>
      New
    </button>
  );
}

(некоторые куски кода были пропущены для краткости)

Что здесь происходит:

  1. При нажатии кнопки вызывается onButtonClick

  2. onButtonClick вызывает переданный "сверху" onAdd и передает ему как первый аргумент значение поля ввода.

Вернемся в TodoApp.jsx и напишем код, который добавляет элемент в список:

TodoApp.jsx
function addNewItem(newItem) {
  setItems([...items, newItem]);
}

Это сложная конструкция, давай разберем по частям:

  1. Функция addNewItem вызывается, когда мы нажимаем на кнопку добавления. newItem — новый элемент.

  2. Внутри неё мы изменяем значение items, теперь оно равно[...items, newItem]

  3. [...items, newItem] — это способ описать массив, который состоит из всех элементов items, а также newItem .

Как отобразить несколько элементов списка сразу

Теперь, когда у нас есть список, в который мы можем добавлять элементы, нам надо научиться их отображать. Компонент элемента списка, кстати, должен выглядеть примерно так:

Item.jsx
export function Item(props) {
  return (
    <div>
      <span>{props.name}</span>
      <button>Delete</button>
    </div>
  );
}

Вернемся в TodoApp. У нас есть массив items, надо "сконвертировать" их в <Item name={...}/> (кстати, не забудь импорировать его) и добавить в дерево. В этом нам поможет функция map.

Представь, что у тебя есть фабрика с конвейером. Пустые бутылки проходят по конвейеру через аппарат, который наливает в них воду. На входе было 100 пустых бутылок, на выходе — 100 полных бутылок. Так вот твой массив — это бутылки на конвейере, а map — это аппарат.

const array1 = [1,2,3,4]

function f(x) {
  return x * x
}

const array2 = array1.map(f)

// array2 = [1,4,9,16]

map вызывает функцию f с каждым элементом массива, над которым она была вызвана, и составляет новый массив из результатов выполнения функции.

В нашем случае есть массив строк, нужно из него сделать массив Item:

TodoApp.jsx
function createItem(name) {
  return <Item name={name} />;
}

Теперь добавим наши элементы в компонент TodoApp:

TodoApp.jsx
return (
    <div className="todo">
      <Field onAdd={addNewItem} />
      {items.map(createItem)}
    </div>
  );

Проверь в браузере, что элементы добавляются.

Дополнительная информация

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

// было: 
['привет', 'привет', 'мир']
// стало: 
['привет', 'мир']
// какой из элементов массива был удален: первый или второй?

Для этого было придумано "магическое" свойство key. Правило простое:

Если у нас есть массив React-компонентов, то у каждого из них должен быть свой, уникальный внутри массива, key.

Давай разберемся, как им пользоваться.

Мы говорили о том, что map вызывает функцию f с каждым элементом массива. Однако, map передает в f не один, а три аргумента:

  1. Элемент массива

  2. Порядковый номер (индекс) элемента

  3. Сам массив.

Перепишем функцию createItem так, чтобы она использовала порядковый номер в качестве key:

TodoApp.jsx
function createItem(name, index) {
  return <Item name={name} key={index}/>;
}

Удаление элементов

В этой главе мы познакомимся с сестрой функции map — функцией filter.

filter вызывает функцию f с каждым элементом массива (а также его индексом и самим массивом), над которым она была вызвана, и составляет новый массив из тех элементов исходного массива, для которых f вернула true.

Пример: отфильтровать массив чисел таким образом, чтобы остались только положительные:

const numbers = [0, 1, 2, 3, 4, -1, -2, -3, -4];

function isPositive(number) {
  return number > 0
}
const positive = numbers.filter(isPositive)
// positive = [1, 2, 3, 4]

Мы хотим написать функцию, которая обновляет список items, удалив из него элемент с указанным индексом (removedIndex):

TodoApp.jsx
export function TodoApp() { 
  
  // ...
  
  function removeItem(removedIndex) {
  
    function f(item, index) {
      return removedIndex !== index;
    }
  
    setItems(items.filter(f))
  }
  
  //return ...
}

Что происходит: filter вызывает f c элементами массива, f в свою очередь, возвращает true для всех элементов, кроме removedIndex.

Мы теперь можем дать нашим Item возможность удалять себя:

TodoApp.jsx
function createItem(name, index) {

  function remove() {
    removeItem(index)
  }

  return <Item name={name} key={index} onDelete={remove}/>;
}

А Item может запрашивать удаление себя по нажатию кнопки:

export function Item(props) {
  return (
    <div>
      <span>{props.name}</span>
      <button onClick={props.onDelete}>
        Delete
      </button>
    </div>
  );
}

Проверь в браузере, что элементы добавляются и удаляются.

О, одна небольшая вещь для самостоятельной работы:

  1. Подумай, как сделать так, что поле ввода очищается после нажатия кнопки добавления.

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

Ответ — в конце следующей главы.

Last updated