Компоненты в React - это основа его структуры. Как и функции в JS, они описывают некие повторяющиеся элементы и выполняют какие-то действия, характерные для этих элементов. На компоненты разобрана страница любого React-приложения. А на этой странице - все ее составляющие: в карточке товара отдельным компонентом будет галерея изображений, кнопка добавления в корзину и выбор цвета, например.
Синтаксис компонента в React
Простейший компонент в React - это некая функция, которая описана в соответствующем файле (Profile.js) и экспортируется из него по умолчанию. Например, это карточка профиля разработчика:
1 2 3 4 5 6 7 8 9 10 | export default function Profile() { return ( <div> <h1>My Profile</h1> <img src="avatar.png" alt="My profile picture" /> <p>Skills: HTML, CSS, JS, React, Vue.js</p> <p><a href="https://github.com/user39238192">Github Link</a></p> </div> ); } |
Обратите внимание на верхнюю строку export default function Profile(). Благодаря ей в другой файл мы можем импортировать код из файла Profile.js и использовать его в виде компонента <Profile /> в коде уже другого компонента.
1 2 3 4 5 6 7 8 9 10 11 | import Profile from './Profile.js'; export default function Gallery() { return ( <section> <h1>Amazing developers</h1> <Profile /> <Profile /> </section> ); } |
Ранее в React использовали не только функциональные компоненты (т.е. определяемые внутри function с неким именем), но и компоненты-классы (тут компонент объявлялся, как класс ). Например:
1 2 3 4 5 6 7 8 | import React, { Component } from 'react'; class Message extends Component { render() { return <p>{this.props.text}</p>; } } |
Однако сейчас сообщество React полностью перешло на функциональные компоненты для большинства сценариев использования. Дело в том, что функциональные компоненты легко интегрируются с такими хуками, как useState, useEffect, и useContext, их легче писать и проще тестировать.
Внутри одного компонента может быть много других. Они могут выглядеть, как одиночные теги (без вложенных элементов) или как обычные HTML-теги, с потомками и с вашими названиями.
Принято называть компоненты с большой буквы. Например:
1 2 3 4 5 6 7 8 9 10 11 | <PageLayout> <NavigationHeader> <SearchBar /> <Link to="/docs">Docs</Link> </NavigationHeader> <Sidebar /> <PageContent> <TableOfContents /> <DocumentationText /> </PageContent> </PageLayout> |
Ваше приложение React формируется благодаря множеству вложенных друг в друга компонентов. React моделирует пользовательский интерфейс в виде дерева, где компоненты имеют родительско-дочерние связи.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Корневой компонент - Root component function App() { return ( <div> <Header /> <Main /> </div> ); } // Компонент верхнего уровня - Top-level component function Header() { return ( <header> <Logo /> <Navigation /> </header> ); } // Дочерний (leaf - листовой) компонент без вложенных, часто используется для повторного рендера function Logo() { return <img src="logo.png" alt="Company Logo" />; } |
Пока все просто. Однако сутью использования компонентов является передача некоторых параметров, или свойств. И вот тут возникает вопрос - кто кому передает эти параметры: родительский компонент дочернему или наоборот.
Передача свойств компоненту
Компоненты React используют свойства для взаимодействия друг с другом. Каждый родительский компонент может передавать некоторую информацию своим дочерним компонентам, передавая им свойства. Свойства могут напоминать HTML-атрибуты, но через них можно передавать любое значение JavaScript, включая объекты, массивы и функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Создаем компонент с рядом свойств function Profile({ name, imageUrl, profession }) { return ( <div className="profile-card"> <h2>{name}</h2> <img src={imageUrl} alt={`${name}'s profile`} /> <p>{profession}</p> </div> ); } // Используем компонент с разными свойствами export default function Gallery() { return ( <section> <h1>Notable Scientists</h1> <Profile name="Maria Skłodowska-Curie" imageUrl="https://i.imgur.com/szV5sdG.jpg" profession="Physicist and Chemist" /> <Profile name="Albert Einstein" imageUrl="https://i.imgur.com/8B7NwTY.jpg" profession="Theoretical Physicist" /> </section> ); } |
Обратите внимание, что свойства в дочернем компоненте перечислены в фигурных скобках { name, imageUrl, profession }. Этот синтаксис называется «деструктуризацией» и эквивалентен чтению свойств из параметра функции:
1 2 3 4 5 6 | function Profile(props) { let name = props.name; let imageUrl = props.imageUrl; let profession = props.profession; // ... } |
Указание значения по умолчанию для свойства
Если вы хотите присвоить свойству значение по умолчанию, к которому можно будет вернуться, когда значение не указано, вы можете сделать это с помощью деструктуризации, поместив =значение по умолчанию сразу после нужного параметра:
1 2 3 | function Avatar({ person, size = 100 }) { // ... } |
Значение по умолчанию используется только в том случае, если sizeсвойство отсутствует или передано size={undefined}. Однако если передано size={null}или size={0}, значение по умолчанию использоваться не будет.
Поддержание чистоты компонентов
Компоненты React работают лучше всего, когда они являются чистыми функциями. Чистая функция всегда возвращает один и тот же результат при одинаковых входных данных и не изменяет ничего, кроме себя.
1 2 3 4 5 6 7 8 9 10 | function TeaCup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>; } // Impure component - modifies external variable (side effect) let guestCount = 0; function ImpureTeaCup({ guest }) { guestCount++; // ❌ This modifies a variable outside the component return <h2>Tea cup for guest #{guest}</h2>; } |
В реальности же существует много вариантов, когда код одних свойств зависит от неких параметров, которые вы можете загружать из базы данных или менять внутри другого компонента. Поэтому довольно часто это значение закрепляют с помощью вызова в компоненте хука useEffect.
Читать по теме:
- Документация React > Поддержание чистоты компонентов
- Документация React >
<StrictMode> - Документация React > Компоненты и хуки должны быть чистыми
- Документация React > React вызывает компоненты и хуки
- Certificates.dev > Чистота компонентов и StrictMode в React
Пример структуры и кода компонента
Используем в этом примере компонент из статьи о синтаксисе JSX в React.
В корневом компоненте мы импортируем данные из файла movies.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | export const ALL_MOVIES = { items: [ { id: 1, name: "The Godfather", description: "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", image: "https://m.media-amazon.com/images/M/MV5BM2MyNjYxNmUtYTAwNi00MTYxLWJmNWYtYzZlODY3ZTk3OTFlXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_FMjpg_UY1982_.jpg", rating: 4, genres: ["Crime", "Drama"], inTheaters: false, }, { id: 2, name: "The Shawshank Redemption", description: "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", image: "https://m.media-amazon.com/images/M/MV5BNDE3ODcxYzMtY2YzZC00NmNlLWJiNDMtZDViZWM2MzIxZDYwXkEyXkFqcGdeQXVyNjAwNDUxODI@._V1_FMjpg_UX1200_.jpg", rating: 4, genres: ["Drama"], inTheaters: false, }, { id: 3, name: "The Dark Knight", description: "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.", image: "https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_FMjpg_UY2048_.jpg", rating: 3, genres: ["Action", "Crime", "Drama"], inTheaters: true, }, ], }; |
Теперь передаем их по одному в компонент MovieItem с помощью метода массива map().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import { ALL_MOVIES } from "./data/movies"; import MovieItem from "./components/MovieItem"; export default function App() { const movies = ALL_MOVIES.items; return ( <div className="app"> <div className="movie-list"> {movies.map((movie) => { return <MovieItem key={movie.id} movie={movie} />; })} </div> </div> ); } |
Компонент MovieItem тоже обязательно надо импортировать, т.к. иначе мы не сможем его использовать в коде - это вызовет ошибку. Обратите внимание, что импорт массива из файла movies.js выполняется с использованием фигурных скобок:
1 | import { ALL_MOVIES } from "./data/movies"; |
Так происходит потому, что в нашем файле movies.js есть константа с ключевым словом export, но не export default.
Сам компонент MovieItem должен быть расположен в папке src вашего проекта на React внутри дочерней папки components в виде файла MovieItem.js. Так принято организовывать структуру в React и такой путь виден и в строке импорта в виде import MovieItem from "./components/MovieItem";.
Код в файле MovieItem.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | import { StarIcon } from "@heroicons/react/24/solid"; export default function MovieItem({ movie }) { return ( <div className="movie-item group"> <div className="movie-item-image-wrapper"> <MovieRatingDisplay rating={movie.rating} /> {movie?.inTheaters && <NowPlayingBanner />} <MovieImage image={movie.image} name={movie.name} /> </div> <div className="movie-item-content-wrapper"> <div className="movie-item-title-wrapper"> <h3 className="movie-item-title">{movie.name}</h3> <div className="movie-item-genres-wrapper"> {movie.genres?.map((genre) => ( <GenreTag key={genre} genre={genre} /> ))} </div> </div> <div className="movie-item-description-wrapper"> <p className="movie-item-description">{movie.description}</p> </div> <div className="movie-item-rating-wrapper"> <span className="movie-item-rating-text"> Rating: {movie.rating || 0}/5 </span> <StarRating rating={movie.rating} movieName={movie.name} /> </div> </div> </div> ); } function MovieRatingDisplay({ rating }) { return ( <div className="movie-item-star-wrapper"> <StarIcon className={`movie-item-star-rating-icon ${ rating ? "text-yellow-500" : "text-gray-500" }`} /> <div className="movie-item-star-content-wrapper"> {rating ? ( <span className="movie-item-star-content-rating-rated">{rating}</span> ) : ( <span className="movie-item-star-content-rating-not-rated">-</span> )} </div> </div> ); } function NowPlayingBanner() { return ( <div className="movie-item-theaters-banner"> <span className="movie-item-theaters-banner-text">Now Playing</span> </div> ); } function MovieImage({ image, name }) { if (image) { return <img src={image} className="movie-item-image" alt={name} />; } return ( <div className="movie-item-no-image"> <span className="movie-item-no-image-text">No image</span> </div> ); } function GenreTag({ genre }) { return <span className="movie-item-genre-tag">{genre}</span>; } function StarRating({ rating, movieName }) { return ( <div className="movie-item-star-icon-wrapper"> {[1, 2, 3, 4, 5].map((star) => ( <button aria-label={`Rate ${movieName} with ${star} star${ star > 1 ? "s" : "" }`} key={star} className={`movie-item-star-icon-button ${ star <= (rating || 0) ? "text-yellow-500" : "text-gray-400 hover:text-yellow-300" }`} > <StarIcon className="movie-item-star-icon" /> </button> ))} </div> ); } |
В коде есть главная функция MovieItem({ movie }), которая содержит указание экспорта по умолчанию (export default). Именно поэтому мы можем ее импортировать в файле App.jsx без фигурных скобок. Однако в том же файле существует ряд второстепенных функций с именами MovieRatingDisplay(), NowPlayingBanner(), MovieImage(), GenreTag(), StarRating(), которые отвечают за разные блоки вывода информации о кинофильме. Их вполне можно было бы вынести в отдельные файлы в папке components, а затем импортировать в файл MovieItem.js. В реальном проекте вы бы так и сделали. Здесь же оставлен более компактный код, чтобы дать пока представление о компонентах React.
Попробуйте самостоятельно разбить код на компоненты и импортировать его в в файл MovieItem.js.
Примечание: в верхней строке происходит импорт import { StarIcon } from "@heroicons/react/24/solid";. Для того чтобы код не выдал ошибку, вам надо установить соответствующий пакет командой в терминале: npm i @heroicons/react