Урок 5: Структура проекта и импорты/экспорты в React

Сегодня мы переходим часто упускаемых из виду аспектов разработки, это организации кода. Этот урок не о том, как писать JSX или использовать хуки, а о том, как сделать так, чтобы ваш проект не превратился в «спагетти-код», где невозможно ничего найти. Мы разберемся со структурой папок и import/export, которые скрепляют все компоненты в единое целое.

Представьте, что вы строите дом. У вас есть кирпичи, окна, двери, кровля, все необходимые материалы. Если вы свалите их в одну большую кучу посреди участка, то каждый раз, когда вам понадобится цемент или доска, вам придется часами разгребать эту гору в поисках нужного. Точно так же и в программировании. Без четкой структуры ваш проект очень быстро станет нечитаемым, его будет сложно масштабировать, а новые разработчики в команде будут теряться в хаосе файлов.

Хорошая структура проекта, это как продуманный план дома с четко обозначенными комнатами, кладовками и чердаком. Она предсказуема. Вы интуитивно понимаете, где искать компонент кнопки, где лежат утилиты для работы с датами, а где стили для всего приложения. Это не только экономит ваше время, но и делает код профессиональным. Когда вы откроете проект через полгода, вы не будете ломать голову, а просто пойдете в папку components/ui и найдете там нужный вам Button.jsx.

Важно понимать, что не существует единственно правильной «идеальной» структуры. Она эволюционирует вместе с вашим проектом. Структура для небольшого личного блога и для корпоративного приложения с сотнями экранов будет отличаться. Однако есть общепринятые лучшие практики и паттерны, которые мы сегодня и изучим. Мы начнем с простой структуры, подходящей для большинства стартапов и наметим пути для ее масштабирования.

Обзор типичной структуры папок в React-проекте

Давайте рассмотрим структуру, которую создает популярный инструмент create-react-app (CRA) и которую можно взять за основу для своих проектов. Открыв новый проект, вы, скорее всего, увидите нечто подобное:

text
my-react-app/
├── public/
│   ├── index.html
│   ├── favicon.ico
│   └── manifest.json
├── src/
│   ├── components/
│   ├── pages/
│   ├── hooks/
│   ├── utils/
│   ├── styles/
│   ├── assets/
│   ├── App.js
│   ├── App.css
│   ├── index.js
│   └── index.css
├── package.json
└── README.md

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

  • public/. Эта папка содержит статические assets. Самый главный файл здесь index.html. Это единственный HTML-файл нашего SPA (Single Page Application). В него будет вмонтировано наше React-приложение. Сюда же обычно кладут иконки (favicon.ico), robots.txt и другие файлы, которые должны быть доступны по прямому URL.

  • src/. Это сердце нашего приложения. Весь наш JavaScript/JSX код живет здесь.

    • components/. Здесь мы храним переиспользуемые UI-компоненты. Это кирпичики нашего интерфейса: кнопки (Button), поля ввода (Input), карточки (Card), заголовки (Header), подвалы (Footer). Внутри эту папку часто структурируют дальше. Например, можно создать подпапку ui для базовых компонентов и features для более сложных, привязанных к бизнес-логике.

    • pages/ (или views/). Компоненты, которые представляют собой целые страницы приложения: HomePageAboutPageContactPageUserProfilePage. Каждая страница собирается из множества мелких компонентов, лежащих в components/.

    • hooks/. С появлением Hooks в React, стала отличной практикой выносить переиспользуемую логику в кастомные хуки. Например, useLocalStorageuseFetchuseAuth. Они помещаются в эту папку.

    • utils/ (или helpers/). Здесь живут чистые JavaScript-функции-утилиты, не связанные напрямую с React. Форматирование дат, работа с текстом, математические вычисления, все это сюда.

    • styles/. Глобальные стили, темы, переменные CSS-in-JS (если вы используете, например, Styled Components) можно хранить здесь.

    • assets/. Статические медиа-файлы, специфичные для приложения: изображения, шрифты, SVG-иконки.

    • App.js. Корневой компонент нашего приложения. Он обычно содержит маршрутизацию (React Router) и общую layout-разметку (шапку, боковую панель, основную область).

    • index.js. Точка входа в приложение. Именно этот файл рендерит наш корневой App-компонент в DOM-элемент с id root, который находится в public/index.html.

По мере роста проекта вы можете добавлять и другие папки: context/ для React Context, services/ или api/ для функций, работающих с бэкендом, constants/ для констант приложения и так далее.

Импорты и экспорты

Если компоненты и утилиты, это органы нашего приложения, то импорты и экспорты, это его кровеносная система. Они позволяют «питательным веществам» (функциям, компонентам, переменным) течь из одного файла в другой. Без них каждый файл был бы изолированным островом и мы не могли бы строить сложные интерфейсы.

В JavaScript и в React существует два основных типа экспортов, именованный (named) и экспорт по умолчанию (default). Давайте разберем их с примерами.

Именованный экспорт (Named Export)

Именованный экспорт используется тогда, когда из одного файла нужно экспортировать несколько сущностей.

Создаем файл src/utils/helpers.js:

javascript
// В этом файле мы объявили несколько функций

// 1. Именованный экспорт (inline)
export const formatDate = (date) => {
  return new Date(date).toLocaleDateString('ru-RU');
};

// 2. Еще один именованный экспорт (inline)
export const capitalizeFirstLetter = (string) => {
  if (!string) return '';
  return string.charAt(0).toUpperCase() + string.slice(1);
};

// 3. Объявление функции, а потом ее экспорт
const calculateDiscount = (price, discount) => {
  return price - (price * discount) / 100;
};

// Экспорт уже объявленной функции (именованный)
export { calculateDiscount };

Как импортировать именованные экспорты в файле src/components/UserCard.js:

javascript
// Импорт с использованием тех же имен, что и при экспорте.
// Имена должны быть заключены в фигурные скобки {}.
import { formatDate, capitalizeFirstLetter } from '../utils/helpers';

// Можно импортировать все именованные экспорты из файла как единый объект
// (этот способ используется реже, но знать его полезно)
import * as helpers from '../utils/helpers';
// Тогда обращаться к функциям нужно так: helpers.formatDate(...)

const UserCard = ({ user }) => {
  return (
    <div className="user-card">
      {/* Используем импортированные функции */}
      <h2>{capitalizeFirstLetter(user.name)}</h2>
      <p>Зарегистрирован: {formatDate(user.registrationDate)}</p>
    </div>
  );
};

export default UserCard;

Ключевые моменты:

  • Имен должно совпадать.

  • Фигурные скобки {} обязательны.

  • Можно переименовать импорт на лету с помощью ключевого слова as:
    import { formatDate as formatRuDate } from '../utils/helpers';

Экспорт по умолчанию (Default Export)

В одном файле может быть только один экспорт по умолчанию. Он обычно используется для экспорта главной сущности файла, чаще всего компонента.

Создаем файл src/components/Button/Button.jsx:

jsx
// Компонент кнопки
const Button = ({ children, onClick, type = 'button' }) => {
  return (
    <button type={type} className="btn" onClick={onClick}>
      {children}
    </button>
  );
};

// Экспорт по умолчанию. Ключевое слово 'default'
export default Button;

Как импортировать экспорт по умолчанию в файле src/pages/HomePage.jsx:

jsx
// При импорте экспорта по умолчанию фигурные скобки НЕ нужны.
// Мы можем дать ему ЛЮБОЕ имя. Хотя принято давать имя, соответствующее экспортируемой сущности.
import Button from '../components/Button/Button';
// Можно было бы написать `import MySuperButton from ...`, но это плохая практика.

const HomePage = () => {
  return (
    <div>
      <h1>Добро пожаловать на главную страницу!</h1>
      {/* Используем нашу кнопку */}
      <Button onClick={() => alert('Клик!')}>Нажми на меня</Button>
    </div>
  );
};

export default HomePage;

Ключевые моменты:

  • Фигурные скобки не используются.

  • Имя при импорте можно выбрать любое, но лучше использовать стандартное.

  • Экспорт по умолчанию идеально подходит для компонентов и страниц.

Смешанный экспорт

В одном файле можно комбинировать экспорт по умолчанию и именованные экспорты.

Файл src/config/constants.js:

javascript
// Именованный экспорт
export const API_BASE_URL = 'https://api.mysite.com';

// Еще один именованный экспорт
export const APP_NAME = 'Мое Крутое Приложение';

// Экспорт по умолчанию (например, основной конфиг-объект)
const mainConfig = {
  version: '1.0.0',
  environment: 'production',
};

export default mainConfig;

Импорт в другом файле:

javascript
// Импортируем экспорт по умолчанию и несколько именованных
import mainConfig, { API_BASE_URL, APP_NAME } from '../config/constants';

console.log(APP_NAME); // 'Мое Крутое Приложение'
console.log(mainConfig.version); // '1.0.0'

Абсолютные и относительные пути

Когда вы пишете импорты, вы указываете путь к файлу. Существует два типа путей: относительные и абсолютные.

  • Относительные пути. Путь указывается относительно текущего файла.

    • ./Component — файл Component в той же папке.

    • ../utils/helpers — подняться на одну папку вверх и зайти в utils/helpers.

    • ../../components/Button — подняться на две папки вверх и зайти в components/Button.

    • Проблема. В больших проектах импорты могут превратиться в нечитаемые конструкции вроде ../../../../services/api, в которых легко ошибиться.

  • Абсолютные пути. Путь указывается от корня проекта (обычно от папки src). Это гораздо удобнее.

    • components/Button

    • pages/HomePage

    • utils/helpers

Как настроить абсолютные импорты?

В проекте, созданном через create-react-app, это делается очень просто. Нужно создать (или отредактировать) файл jsconfig.json в корне проекта (рядом с package.json).

json
{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": ["src"]
}

После этого вы можете переписать ваши «уродливые» относительные импорты на красивые абсолютные:

Было:
import Button from '../../../components/Button/Button';

Стало:
import Button from 'components/Button/Button';

Это делает код чище и уменьшает количество ошибок.

Практические задачи для закрепления

Давайте применим полученные знания на практике. Создадим небольшую структуру и свяжем все компоненты вместе.

Задача 1: Создайте структуру проекта

  1. В папке src создайте папки: componentspagesutils.

  2. Внутри components создайте папку ui.

  3. Внутри pages создайте файлы HomePage.jsx и AboutPage.jsx.

Задача 2: Создайте компонент кнопки с экспортом по умолчанию

  1. В папке src/components/ui создайте файл Button.jsx.

  2. Напишите простой функциональный компонент Button, который принимает пропсы children и onClick.

  3. Экспортируйте его по умолчанию.

Код для Button.jsx:

jsx
const Button = ({ children, onClick }) => {
  return (
    <button
      onClick={onClick}
      style={{
        padding: '10px 15px',
        backgroundColor: 'blue',
        color: 'white',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer'
      }}
    >
      {children}
    </button>
  );
};

export default Button;

Задача 3: Создайте утилиты с именованным экспортом

  1. В папке src/utils создайте файл stringHelpers.js.

  2. Создайте и экспортируйте именованным способом функцию truncateText(text, maxLength), которая обрезает текст до maxLength символов и добавляет в конце троеточие.

Код для stringHelpers.js:

javascript
export const truncateText = (text, maxLength) => {
  if (text.length <= maxLength) {
    return text;
  }
  return text.substr(0, maxLength) + '...';
};

Задача 4: Создайте страницу, которая использует и кнопку и утилиту

  1. Откройте файл src/pages/HomePage.jsx.

  2. Импортируйте компонент Button с помощью абсолютного пути (components/ui/Button).

  3. Импортируйте функцию truncateText с помощью абсолютного пути (utils/stringHelpers).

  4. Создайте компонент HomePage, который внутри имеет абзац текста, обрезанный с помощью truncateText и нашу кастомную кнопку.

  5. Экспортируйте HomePage по умолчанию.

Код для HomePage.jsx:

jsx
// Используем абсолютные импорты (предварительно настроив jsconfig.json)
import Button from 'components/ui/Button';
import { truncateText } from 'utils/stringHelpers';

const HomePage = () => {
  const longText = "Это очень длинный текст, который определенно нужно обрезать для того, чтобы он поместился в нашем красивом интерфейсе.";

  return (
    <div>
      <h1>Главная страница</h1>
      {/* Используем утилиту */}
      <p>{truncateText(longText, 50)}</p>
      {/* Используем компонент */}
      <Button onClick={() => console.log('Кнопка на главной нажата!')}>
        Я кнопка с главной
      </Button>
    </div>
  );
};

export default HomePage;

Задача 5: Соберите все вместе в App.js

  1. Откройте src/App.js.

  2. Импортируйте HomePage с помощью абсолютного пути.

  3. Отобразите компонент HomePage внутри App.

Код для App.js:

jsx
import HomePage from 'pages/HomePage';
import './App.css';

function App() {
  return (
    <div className="App">
      <HomePage />
    </div>
  );
}

export default App;

Поздравляю! Вы только что создали свою первую осознанно структурированную часть React-приложения. Вы не просто написали код, вы организовали его, сделав понятным и легко расширяемым.

Типичные ошибки и как их избежать

  1. Путаница между export default и export. Помните, для импорта дефолтного экспорта скобки не нужны, для именованного обязательны. Если вы видите ошибку SyntaxError или undefined, первым делом проверьте правильность экспорта/импорта.

  2. «Адские» относительные пути. Как только вы видите больше двух ../, самое время задуматься о настройке абсолютных импортов через jsconfig.json.

  3. Циклические зависимости. Компонент A импортирует компонент B, который, в свою очередь, импортирует компонент A. React (и JavaScript в целом) может не обработать это корректно, что приведет к ошибкам. Следите за архитектурой ваших компонентов.

  4. Неиспользование папок для группировки связанных файлов. Если компонент состоит из самого файла компонента, стилей и тестов, лучше положить его в отдельную папку. Например, Button/index.jsxButton/Button.module.cssButton/Button.test.jsx. Тогда импорт останется чистым: import Button from 'components/Button';.

Структура и модульность это то, что отделяет новичка от уверенного разработчика. Потратив немного времени на организацию кода в начале пути, вы сэкономите себе десятки, а то и сотни часов в будущем. Не пренебрегайте этим.

Удачи в изучении и помните, что чистота кода это залог вашего спокойствия в будущем. Чтобы продолжить обучение и погрузиться в следующие темы, такие как состояние, хуки и работа с формами, переходите по ссылке на полный курс с уроками по React для начинающих.

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий