Олег Марков
Как писать код на React правильно
Введение
Вам когда-нибудь казалось, что ваш код на React можно было бы сделать чище и проще для поддержки? Или возникали проблемы с производительностью и запутанностью компонентов? Даже если вы только начинаете или уже работали с этим библиотекой, понимание того, как писать код на React правильно, позволит создавать более масштабируемые, читаемые и поддерживаемые приложения.
В этой статье я разберу основные принципы грамотного написания кода на React. Вы увидите, на что стоит обращать внимание при разработке, как структурировать проект, правильно организовывать компоненты, управлять состояниями, стилизовать элементы и оптимизировать производительность. Приведу наглядные примеры, чтобы вы могли применить эти подходы на практике.
Организация проекта и структура директорий
Когда проект на React становится больше пары десятков компонентов, от структуры директорий напрямую зависит удобство разработки и сопровождения.
Общая структура приложения
Один из популярных стандартов — деление проекта на папки с компонентами, страницами и утилитами. Примерно так:
src/
components/ // Переиспользуемые компоненты
pages/ // Отдельные страницы приложения
hooks/ // Кастомные хуки
utils/ // Вспомогательные функции
assets/ // Изображения, иконки, стили
App.js
index.js
Каждый компонент (особенно если он крупный и связан с логикой/стилями/тестами) лучше помещать в отдельную папку:
components/
Button/
Button.jsx
Button.module.css
Button.test.jsx
Такую структуру легко расширять и поддерживать. Если проект использует маршрутизацию (React Router), для страниц удобно использовать отдельную папку pages
.
Разделяйте логику и представление
Когда файл компонента разрастается, логику удобно выносить в кастомные хуки (папка hooks) или отдельные утилиты (utils). Это упрощает тестирование и уменьшает дублирование кода.
Смотрите, как можно вынести логику в хук:
// hooks/useUser.js
import { useState, useEffect } from 'react';
export function useUser(id) {
const [user, setUser] = useState(null);
useEffect(() => {
// Загружаем пользователя по id
fetch(`/api/user/${id}`)
.then(res => res.json())
.then(setUser);
}, [id]);
return user;
}
// components/UserProfile/UserProfile.jsx
import React from 'react';
import { useUser } from '../../hooks/useUser';
function UserProfile({ id }) {
const user = useUser(id);
if (!user) return <div>Загрузка...</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
Теперь логику работы с пользователем можно легко переиспользовать.
Написание чистого, поддерживаемого и эффективного кода является ключевым для успешной разработки React-приложений. Соблюдение лучших практик и использование правильных подходов позволяет создавать более надежные и масштабируемые приложения. Если вы хотите научиться писать код на React правильно и следовать лучшим практикам разработки — приходите на наш большой курс Основы React, React Router и Redux Toolkit. На курсе 177 уроков и 17 упражнений, AI-тренажеры для безлимитной практики с кодом и задачами 24/7, решение задач с живым ревью наставника, еженедельные встречи с менторами.
Создание и организация компонентов
В React каждый элемент интерфейса — это компонент. Старайтесь следовать принципу "одна задача — один компонент": маленькие, переиспользуемые и простые компоненты легче тестировать, поддерживать и развивать.
Функциональные компоненты как стандарт
С приходом хуков большинство новых проектов строят на функциональных компонентах — они проще классовых и легче читаются.
Пример:
// Простой функциональный компонент
function Greeting({ name }) {
return <p>Привет, {name}!</p>; // Возвращает JSX-разметку
}
Если в компоненте нет сложной логики, пишите его одной строкой:
const Hello = ({ name }) => <span>Привет, {name}!</span>;
Разделяйте умные и глупые компоненты
Умные (container) компоненты отвечают за данные, работу с API, стейт.
Глупые (presentational) компоненты только рендерят то, что получили через props.
Вот так:
// components/UserList/UserList.jsx - умный компонент
import { useEffect, useState } from 'react';
import { UserItem } from './UserItem';
export function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return (
<ul>
{users.map(user => <UserItem key={user.id} user={user} />)}
</ul>
);
}
// components/UserList/UserItem.jsx - глупый компонент
export function UserItem({ user }) {
return <li>{user.name}</li>;
}
Такой подход делает код проще для переиспользования: UserItem можно использовать в других местах, даже если данных о пользователях будет мало.
Используйте PropTypes или TypeScript
Чтобы избежать неожиданных ошибок и улучшить подсказки в редакторе, типизируйте пропсы. Можно использовать PropTypes
:
import PropTypes from 'prop-types';
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
Button.propTypes = {
onClick: PropTypes.func.isRequired,
children: PropTypes.node
};
Еще надежнее — использовать TypeScript и объявлять типы пропсов интерфейсами:
type ButtonProps = {
onClick: () => void;
children: React.ReactNode;
};
const Button: React.FC<ButtonProps> = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);
Стейт и управление данными
Часто начинающие разработчики путаются в том, где создавать state и как оптимально передавать данные между компонентами. Вот несколько принципов.
State должен находиться на минимальном необходимом уровне
Старайтесь держать состояние как можно ближе к компоненту, который его использует.
Пример: если список элементов фильтруется внутри компонента списка, держать состояние фильтра стоит именно там.
Если одно и то же состояние нужно нескольким компонентам — выносите его выше в дерево компонентов или используйте контекст.
// State поднимается выше, если нужен нескольким потомкам
function Parent() {
const [value, setValue] = useState('');
return (
<>
<Input value={value} onChange={setValue} />
<FilteredList filter={value} />
</>
);
}
Используйте только необходимые хуки
useState
— для локального состояния компонента.useReducer
— для сложного локального состояния и имитации Redux внутри компонента.useContext
— для передачи общих данных (например, темы оформления) по дереву компонентов, избегая "прокидывания пропсов".
Пример использования контекста:
// context/ThemeContext.js
import { createContext } from 'react';
export const ThemeContext = createContext('light');
// App.jsx
import { ThemeContext } from './context/ThemeContext';
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// components/Toolbar.jsx
import { useContext } from 'react';
import { ThemeContext } from '../context/ThemeContext';
function Toolbar() {
const theme = useContext(ThemeContext);
// theme будет "dark"
return <div className={theme}>Панель инструментов</div>;
}
Не бойтесь внешних библиотек для управления состоянием
Если стейт большой, и его нужно синхронизировать между множеством компонентов (например, корзина магазина, сессии пользователя) — используйте Redux, MobX, Zustand или Recoil. При этом оптимально изолировать доступ только к нужным данным, а не строить "глобальный стор" для всего приложения.
Стилизация компонентов
React не ограничивает вас в выборе подхода к стилизации компонентов. Вот основные способы:
CSS Modules
CSS Modules хорошо подходят для изоляции стилей каждого компонента:
/* Button.module.css */
.button {
background: #2196f3;
color: white;
}
import styles from './Button.module.css';
function Button({ children }) {
return <button className={styles.button}>{children}</button>;
}
Styled-components и аналогичные библиотеки
Эти инструменты позволяют писать CSS прямо в JavaScript-файлах и создавать "стилизованные компоненты".
import styled from 'styled-components';
const Button = styled.button`
background: #2196f3;
color: white;
padding: 10px 20px;
`;
export function MyButton({ children }) {
return <Button>{children}</Button>;
}
Tailwind CSS
Утилитарный CSS-фреймворк позволяет стилизовать, не выходя из JSX:
function Button({ children }) {
return (
<button className="bg-blue-500 text-white px-4 py-2 rounded">
{children}
</button>
);
}
Важно: Не смешивайте разные способы стилизации в одном проекте — придерживайтесь одного подхода, чтобы сохранить читаемость кода.
Использование эффектов и асинхронных операций
Один из ключевых хуков — useEffect
, который управляет побочными эффектами (например, сетевыми запросами, подписками).
Основы использования useEffect
import { useEffect, useState } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
// Выполнить запрос при монтировании
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // [] — эффект сработает ТОЛЬКО при первом рендере
return <div>{data ? JSON.stringify(data) : 'Загрузка...'}</div>;
}
Управляйте зависимостями правильно
Массив зависимостей ([]
) в useEffect говорит React, когда запускать эффект. Оставляйте этот массив всегда точным и минимальным — если в нем не хватает переменных, эффект будет "видеть" устаревшие значения.
useEffect(() => {
// Если использовать `value` внутри эффекта, его стоит добавить в зависимости
// Иначе useEffect может реагировать на устаревшие value
}, [value]);
Очищайте ресурсы (cleanup)
Если эффект запускает подписку, таймер или сетевой запрос — обязательно очищайте ресурсы после размонтирования компонента.
useEffect(() => {
const id = setInterval(() => {
// что-то обновляем
}, 1000);
return () => clearInterval(id); // очистка при размонтировании
}, []);
Оптимизация производительности
Реальные приложения могут тормозить, если рендерить компоненты слишком часто или неправильно хранить данные/эффекты.
Мемоизация (React.memo, useMemo, useCallback)
React.memo предотвращает повторный рендер компонента, если его пропсы не изменились.
import React from 'react';
const MyComponent = React.memo(function MyComponent({ value }) {
// если value не изменяется — компонент не будет перерисован
return <div>{value}</div>;
});
useMemo — мемоизирует результат функции.
const expensiveValue = useMemo(() => calculateValue(a, b), [a, b]);
useCallback — мемоизирует функцию:
const handleClick = useCallback(() => {
// действие при клике
}, [value]); // будет создана новая функция только при изменении value
Оптимальное использование ключей (key) при рендере списков
Никогда не используйте индексы массива в качестве key, если список может изменяться — это вызовет баги при удалении/добавлении элементов.
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
Lazy loading и code splitting
Для больших проектов используйте React.lazy
и динамический импорт.
import React, { Suspense, lazy } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Загрузка...</div>}>
<HeavyComponent />
</Suspense>
);
}
Это уменьшает время загрузки страницы.
Форматирование, линтинг и тестирование
Используйте форматтеры и линтеры
Инструменты как Prettier (форматтер) и ESLint (линтер) позволят поддерживать единый стиль кода в команде, ловить ошибки еще до запуска приложения.
Пример .eslintrc.js:
module.exports = {
extends: ['react-app', 'plugin:react/recommended'],
rules: {
// свои правила
},
};
Покрывайте критичный код тестами
React предоставляет удобные инструменты для тестирования компонентов — например, React Testing Library или Enzyme (устаревающий). Чем проще и меньше компонент — тем легче его покрыть тестом.
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('отображает переданный текст', () => {
render(<Button>Сохранить</Button>);
expect(screen.getByText('Сохранить')).toBeInTheDocument();
});
Работа с пропсами по умолчанию и значениями по умолчанию
Задавайте значения по умолчанию для пропсов, если компонент должен работать "как есть" без обязательных пропсов.
function Card({ children, style = {} }) {
// style будет пустым объектом по умолчанию
return <div style={style}>{children}</div>;
}
Организация импортов и порядок кода
Всегда размещайте импорты в начале файла. Сначала библиотечные, затем внутренние/пользовательские. Это облегчает навигацию:
import React from 'react'; // стандартные библиотеки
import { useUser } from '../../hooks/useUser'; // внутренние импорты
import styles from './Button.module.css'; // стили
Порядок в файле:
- Импорты
- Вспомогательные функции
- Основной функциональный компонент
- Экспорт по умолчанию или именованный экспорт
Использование современных возможностей React
Следите за нововведениями: например, React 18 дает авто-ускорение обновлений, возможность рендерить компоненты асинхронно (useTransition
), Suspense для данных. Это позволяет избежать устаревших паттернов.
Заключение
Грамотное написание кода на React складывается из многих составляющих: аккуратной структуры проекта, продуманной работы со стейтом и пропсами, следования современным практикам и оптимизации производительности. Чем лучше организован ваш код и ваши компоненты, тем проще масштабировать проект, находить и устранять баги, внедрять новые фичи и работать в команде. Регулярно анализируйте свой код, внедряйте автоматические инструменты контроля качества, не стесняйтесь экспериментировать с современными возможностями React. Как вы видите, даже небольшие улучшения в стиле и подходах к написанию кода заметно влияют на итоговое качество приложения.
Написание качественного кода является основой разработки. Для создания сложных приложений требуется умение управлять состоянием и роутингом. Курс Основы React, React Router и Redux Toolkit поможет вам в этом. В первых 3 модулях уже доступно бесплатное содержание — начните погружаться в основы React уже сегодня.
Частозадаваемые технические вопросы по теме статьи
1. Как добавить глобальный обработчик ошибок в приложение на React?
Добавьте компонент-обертку с использованием класса ErrorBoundary. Пример:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <h1>Что-то пошло не так.</h1>;
}
return this.props.children;
}
}
// Использование:
<ErrorBoundary>
<App />
</ErrorBoundary>
2. Как работать с формами и контролируемыми компонентами в React?
Используйте local state для хранения значений инпутов. Пример:
function MyForm() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
3. Что делать, если возникает бесконечный ререндер при использовании useEffect?
Проверьте массив зависимостей во втором аргументе useEffect. Не добавляйте туда функции или объекты, которые пересоздаются при каждом рендере. Мемоизируйте их с помощью useCallback/useMemo или передавайте только необходимые примитивы.
4. Как реализовать переиспользуемые композиции логики между компонентами?
Создайте кастомные хуки. Функции типа useCustomHook
могут содержать повторяемую логику, которую затем можно применять в разных компонентах.
5. Как правильно организовать динамические маршруты с React Router?
В файле маршрутов используйте параметры через двоеточие:
<Route path="/user/:id" element={<UserProfile />} />
Внутри компонента используйте хук useParams для получения параметров маршрута:
import { useParams } from 'react-router-dom';
function UserProfile() {
const { id } = useParams();
// далее: загрузить пользователя с id
}
Постройте личный план изучения React до уровня Middle — бесплатно!
React — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по React
Лучшие курсы по теме

React и Redux Toolkit
Антон Ларичев
TypeScript с нуля
Антон Ларичев