Плейлист со всеми уроками: https://www.youtube.com/playlist?list=PLvWwA9iDlhHBQ6razvwomGcUIfQm4fk6D
GitHub-репозиторий с готовыми приложениями: http://bit.ly/gh_course_react_beginner
Рендеринг списков из компонентов
Теперь разберемся с очень часто встречающейся задачей — рендеринг нескольких компонентов по входному набору данных, в качестве которого обычно выступает массив.
Давайте сейчас попробуем каждый элемент массива с данными о пользователе, обернуть в компонент и отрисовать на странице. Объявим массив users в компоненте App:
const users = [
{
name: 'Alex Feel',
age: 29,
specialty: 'Animator'
},
{
name: 'Nick Kras',
age: 27,
specialty: 'Programmer'
},
{
name: 'Dmitry Put',
age: 26,
specialty: 'Art Director'
}
]
Далее, оформим сам компонент App таким образом:
class App extends React.Component {
render() {
return (
<div className='wrapper'>
{users.map((user, i) => <Card key={i} {...user} />)}
</div>
)
}
}
users это массив и у него на прототипе есть метод map, который возвращает новый массив, пройдясь по всем элементами старого. Важно то, что он не меняет старый массив. Это относится к концепции иммутабельности (immutability), то есть неизменяемости данных, которую мы подробнее рассмотрим позже.
В доках React приводят такой наглядный пример работы map:
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map((number) => number * 2)
console.log(numbers)
console.log(doubled)
Выполните в консоли браузера и получите старый массив и новый, в котором все числа будут удвоены. Наглядный пример работы этого метода.
Итак, на каждой итерации прохода по users мы выполняем колбэк, где возвращается компонент Card. В нем содержится разметка для каждой карточки юзера. С помощью механизма деструктуризации мы отправляем все данные, касающиеся юзера в компонент Card.
А вот как выглядит он:
import React from 'react'
const Card = (props) => {
const { name, age, specialty } = props
return (
<div className='item'>
<img src='http://i.pravatar.cc/300' alt='' />
<h2>{name}</h2>
<p>Age: {age}</p>
<p>Specialty: {specialty}</p>
</div>
)
}
export default Card
В нем мы деструктурируем то что приехало в пропсы — а это данные по нашему юзеру. И эти переменные уже в разметке используем. Тут простой презентационный компонент.
Не забудем импортировать Card в App. А также напишем простейшие стили в App.css:
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.item {
width: 350px;
}
Собственно, вот что у нас получилось. Как и ожидали.
Обратите внимание, как мы обернули users.map в фигурные скобки — именно так можно внутри JSX-кода записать map. Это достаточно удобно, но можно конечно было записать результат map в переменную, а затем уже вывести ее. Это бы выглядело так:
const usersList = users.map((user, i) => <Card key={i} {...user} />)
class App extends React.Component {
render() {
return (
<div className='wrapper'>
{usersList}
</div>
)
}
}
И это также работает. Если попробовать вывести в консоль usersList, то получим массив, как и ожидалось, но он будет заполнен специальными React’овскими объектами, из которых библиотека сможет извлечь всю нужную информацию и отрисовать наш список. Разбираться там внутри мы не будем — это сейчас не столь важно.
Ключи (keys)
Есть еще кое-что, что критически важно разобрать. Это пропс key (то есть ключ) в колбэке метода map. Он не простой — Реакт ожидает его для каждого компонента, который мы рендерим. И задача состоит в уникальной идентификации каждого компонента списка.
Скажу лишь только, что если мы соберемся, например сортировать наших пользователей, то использование индексов из map‘а будет неоптимальным ходом. Особенно если речь будет идти о сотне элементов в списке.
Избавиться от негативного эффекта можно, если использовать уникальный индекс, связанный с отрисовываемым на каждой итерации элементом.
В нашем случае это может быть id, привязанный к каждому пользователю.
Давайте изменим текущий key для наших компонентов Card на поле user.id, которое мы всем нашим юзерам заранее добавим.
const users = [
{
id: 1,
name: 'Alex Feel',
age: 29,
specialty: 'Animator'
},
{
id: 2,
name: 'Nick Kras',
age: 27,
specialty: 'Programmer'
},
{
id: 3,
name: 'Dmitry Put',
age: 26,
specialty: 'Art Director'
}
]
class App extends React.Component {
render() {
return (
<div className='wrapper'>
{users.map((user) => <Card key={user.id} {...user} />)}
</div>
)
}
}
Так делать лучше всего. Детально данный механизм мы будем рассматривать в других частях этого курса. Пока что просто поверьте, что это правда.
Если key не указать, то Реакт сам их расставит, но при этом выдаст предупреждение в консоль. Лучше конечно расставлять ключи явно.
Key должен быть уникальным только среди элементов на одном и том же уровне. В нашем случае это все карточки с пользователями. То есть, если даже мы используем индексы, генерируемые методом map здесь и еще в каком-то месте, то проблем не будет. То есть вот так проблем не будет:
class App extends React.Component {
render() {
return (
<div className='wrapper'>
<div className='wrapper'>
{users.map((user, i) => <Card key={i} {...user} />)}
</div>
<div className='wrapper'>
{users.map((user, i) => <Card key={i} {...user} />)}
</div>
</div>
)
}
}
Ну и последнее насчет key. Его нужно объявлять вовне, а не внутри:
class App extends React.Component {
render() {
return (
<div className='wrapper'>
{users.map((user) => <Card keyValue={user.id} {...user} />)}
</div>
)
}
}
const Card = (props) => {
const { name, age, specialty, keyValue } = props
return (
<div className='item' key={keyValue}>
<img src='http://i.pravatar.cc/300' alt='' />
<h2>{name}</h2>
<p>Age: {age}</p>
<p>Specialty: {specialty}</p>
</div>
)
}
Это неправильный пример, когда мы пробрасываем key внутрь компонента и ставим его на внешней обертке. Данный ход не имеет смысла и вы все равно получите предупреждение в консоли.
Формы. Контролируемые и неконтролируемые компоненты.
Контролируемые компоненты
Элементы форм в браузере, если вдуматься, отличаются тем, что имеют некий свой внутренний стейт, выражаясь терминами Реакта.
Это касается input, textarea и select. Когда вы в них что-то вводите, это значение сохраняется и выводится внутри элемента.
input
class App extends React.Component {
state = { value: '' }
handleChange = ({ target: { value } }) => this.setState({ value })
handleSubmit = () => alert('A name was submitted: ' + this.state.value)
render = () => (
<form>
<label>
Name:
<input type='text' value={this.state.value} onChange={this.handleChange} />
</label>
<button type='button'>Send</button>
</form>
)
}
Перед вами пример простейшего контролируемого компонента. Смысл тут очень простой.
На onChange, то есть изменение input мы вешаем обработчик клика, который при каждом новом вводе сет-стейтит новое значение, полученное из event-объекта (я его хитро деструктурировал, чтобы уменьшить количество кода).
А при каждой отрисовке значение инпута берется из стейта. Выходит что инпут буквально находится под контролем у стейта — отправляет новые значения в него и читает оттуда же.
На вкладке Реакт в Инструментах Разработчика хорошо видно как меняется стейт.
Неконтролируемые компоненты
Ну раз уж мы говорили про контролируемые компоненты, значит есть и неконтролируемые, логично?
Вне Реакта, чтобы взять значение из инпута, нужно найти его в DOM-дереве и считать это значение. Как же поступать в контексте Реакта?
Ответ прост — ref (от англ. reference — ссылка). Это по сути то, что позволяет внутри компонента уникально пометить любой элемент разметки или компонент. Это мы разберем в следующих уроках.
Так вот, с помощью рефов мы и берем значения из нужных полей. Пример:
class App extends React.Component {
constructor(props) {
super(props)
this.input = React.createRef()
}
handleSubmit = (e) => {
alert('A name was submitted: ' + this.input.current.value)
e.preventDefault()
}
render = () => (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type='text' ref={this.input} />
</label>
<input type='submit' value='Submit' />
</form>
)
}
В конструкторе компонента на this, то есть на компонент, мы присваиваем поле input, которое благодаря вызову Реактовской функции createRef, создает структуру, содержащую ссылку на элемент.
А вот на какой? Выясняется это при первом рендере — вот атрибут ref, который явно указывает что в this.input надо записать ссылку на данный элемент.
При изменении текста в инпуте, срабатывает метод handleSubmit. Внтри него мы как раз по ссылке this.input.current и берем value с нужным нам текстом. Замечу, что писать current обязательно.
Также, при создании неконтролируемого компонента, если хочется задать начальное значение, нужно использовать атрибут defaultValue вместо value, иначе вы получите описанную ранее ситуацию, где в инпуте ничего нельзя будет поменять.