5 лучших практик для React разработчиков

2 года назад·6 мин. на чтение

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

Существует множество способов структурировать код так, чтобы он был читабельным, и у каждого свой подход к этому. Возникает вопрос, есть ли лучший способ сделать это? Мы поговорим о лучших практиках:
  • Организация каталога
  • Компоненты и разделение ответственности
  • Работа с состояниями
  • Абстракция
  • Соглашения об именовании

1. Организация каталогов

В документации React упоминается, что в целом существует два основных способа организации вашего приложения React: Группировка по функциям или маршрутам и Группировка по типу файлов. Главное здесь - не перемудрить. Если вы начинаете с небольшого приложения, вы можете органично организовать свой код по ходу работы так, как вам удобно. Помните, что React не имеет собственной жесткой структуры, поэтому на 100% зависит от вас, как все будет структурировано. Если у вас есть логическое объяснение тому, как организованы файлы, то все хорошо. Однако, поскольку в документации React упоминаются эти две стратегии организации, давайте рассмотрим каждую из них, чтобы понять, как они структурированы. Допустим, у нас есть приложение для электронной коммерции, в котором есть пользователь, список товаров, подробная страница товара, корзина и процесс оформления заказа.

Группировка по функциям

Корневой каталог здесь - это каталог src, который является одним из двух базовых каталогов в вашем приложении React (второй - папка public). В каталоге src у нас будут основные файлы App.js и index.js в корне папки. Затем у нас будут вложенные каталоги для каждой из функций вашего приложения. Ваш подход к структуре может варьироваться в зависимости от того, как все организовано: в проекте может быть больше каталогов, меньше каталогов или даже более глубокая вложенность на компоненты, стилизацию и тестирование.
src/
  App.js
  App.css
  App.test.js
  index.js
  global/ <= общие для всего приложения сущности
    AppContext.js
    ThemeContext.js
    UserContext.js
    Button.js
  cards/
    index.js
    Cards.css
    Cards.js
    Card.css
    Card.js
    Card.test.js
  detailed-product/
    DetailedProduct.css
    DetailedProduct.js
    DetailedProduct.test.js
  checkout/
    ReviewOrder.css
    ReviewOrder.js
    ReviewOrder.test.js
    ShoppingCart.css
    ShoppingCart.js
    ShoppingCart.test.js
  user/
    index.js
    User.css
    User.js
    User.test.js

Группировка по типу файлов

Корневой каталог по-прежнему является каталогом src. Все, что будет отображаться на экране клиента, по-прежнему находится в этой папке. Как и раньше, мы будем хранить файлы App.js и index.js в корне этого каталога, а затем каталоги, представляющие составные части приложения: компоненты, контекст, CSS, хуки и тесты.
src/
  App.js
  index.js
  components/
    App.css
    Card.js
    Cards.js
    ConfirmationPage.js
    DetailedProduct.js
    Footer.js
    Navbar.js
    ReviewOrder.js
    Settings.js
    ShoppingCart.js
    User.js
  context/
    AppContext.js
    ThemeContext.js
    UserContext.js
  css/
    Card.css
    Cards.css
    ConfirmationPage.css
    DetailedProduct.css
    Footer.css
    Navbar.css
    ReviewOrder.css
    Settings.css
    ShoppingCart.css
    User.css
  hooks/
    useAuth.js
    useAxios.js
  tests/
    App.test.js
    Card.test.js
    Cards.test.js
    ConfirmationPage.test.js
    DetailedProduct.test.js
    Footer.test.js
    Navbar.test.js
    ReviewOrder.test.js
    Settings.test.js
    ShoppingCart.test.js
    User.test.js
Как и прежде, способ настройки проекта зависит от вашего приложения и от того, как вы хотите его реализовать. Основная структура здесь зависит от типа файла и не более того. В конечном итоге структура вашего файла должна быть сделана так, чтобы в ней было легко ориентироваться. Как вы это сделаете, зависит только от вас. О других вариантах структурирования React приложения читайте в:

2. Компоненты и разделение ответственности

До появления React Hooks было довольно легко определить, что считается компонентом класса с состоянием, а что - презентационным функциональным компонентом. Некоторые разработчики также называли их "умными" компонентами и "глупыми" компонентами. Разумеется, умные компоненты - это те, которые несут состояние и обрабатывают логику, а глупые компоненты - это те, которые просто принимают передаваемые им пропсы. После появления React Hooks и обновления Context API почти все можно считать функциональным компонентом, что приводит к разговору о том, когда следует разделять компоненты, содержащие локальное состояние, и компоненты, которые его не содержат, и как это делать. В конечном счете, это зависит от вас и/или вашей команды, как построить ваш паттерн проектирования, но лучшая практика показывает, что логика и компоненты с локальным состоянием должны быть отделены от статических компонентов. Подробнее о разделении ответственности читайте в статье Разделение ответственности в React. Как использовать контейнерные и презентационные компоненты..

3. Работа с состоянием и пропсами

Поток данных в React-приложении очень важен. Есть два способа работы с данными: использование состояния или передача состояния в виде пропсов. Давайте рассмотрим лучшие практики.

Состояние

При работе с состоянием, будь то глобально в контекстном API или локально, его нельзя изменять напрямую, переназначая свойство state с новым значением:
addOne = () => { // Так обновлять состояние нельзя
  this.state.counter += 1;
}
Вместо этого при работе с состоянием в классовых компонентах используйте метод this.setState() для обновления состояния.
import React from "react";
import "./styles.css";
class Counter extends React.Component{
 constructor(props) {
   super(props);
   this.state = {
     counter: 0
   }
 }
 addOne = () => {
   this.setState({counter: this.state.counter + 1})
 }
 subtractOne = () => {
   this.setState({counter: this.state.counter - 1});
 }
 reset = () => {
   this.setState({ counter: 0 });
 }
 render() {
   return (
     <div className="App">
       <h1>Simple React Counter</h1>
       <h2>{this.state.counter}</h2>
       <button onClick={this.addOne}> + </button>
       <button onClick={this.reset}> Reset </button>
       <button onClick={this.subtractOne}> - </button>
     </div>
   );
 }
}
export default Counter;
При использовании React Hooks вы будете использовать любое название своего set метода:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
 const [ counter, setCounter ] = useState(0);
 const addOne = () => {
   setCounter(counter + 1)
 }
 const subtractOne = () => {
   setCounter(counter - 1);
 }
 const reset = () => {
   setCounter(0);
 }
   return (
     <div className="App">
       <h1>Simple React Counter</h1>
       <h2>{counter}</h2>
       <button onClick={subtractOne}> - </button>
       <button onClick={reset}> Reset </button>
       <button onClick={addOne}> + </button>
     </div>
   );
}

Пропсы

При работе с пропсами и передаче состояния другим компонентам для использования, может наступить момент, когда вам потребуется передать пропсы пяти дочерним компонентам. Такой метод передачи пропсов от родителя к ребенку на протяжении нескольких поколений называется prop drilling, и его следует избегать. Хотя код, безусловно, будет работать, если вы будете передавать проп на много уровней вниз, он подвержен ошибкам, и поток данных может быть трудно отслеживать. Вам следует создать какой-либо паттерн проектирования для глобального управления состоянием с помощью Redux или Context API (Context API в настоящее время является более простым и предпочтительным способом). Подробнее о вариантах передачи данных между реакт компонентами читайте в статье Как передавать данные между компонентами в ReactJS

4. Абстракция

React процветает благодаря возможности повторного использования. Когда мы говорим о лучших практиках React, часто встречается термин абстракция. Абстракция означает, что есть части большого компонента или приложения, которые могут быть изъяты, превращены в собственный функциональный компонент, а затем импортированы в более крупный компонент. Если сделать компонент как можно проще, часто так, чтобы он служил только одной цели, это увеличивает шансы на многократное использование кода. В простом приложении счетчика, созданном выше, есть возможность абстрагировать некоторые элементы от компонента App. Кнопки могут быть абстрагированы в собственный компонент, где мы передаем метод и метку кнопки в качестве пропсов. Заголовок и название приложения также могут быть размещены в собственных компонентах. После того как мы абстрагировали все элементы, компонент App может выглядеть примерно так:
import React, { useState } from "react";
import { Button } from "./Button";
import { Display } from "./Display";
import { Header } from "./Header";
import "./styles.css";

export default function App() {
  const addOne = () => {
   setCounter(counter + 1)
 }
 const subtractOne = () => {
   setCounter(counter - 1);
 }
 const reset = () => {
   setCounter(0);
 }
 const initialState = [
   {operation: subtractOne, buttonLabel:"-"},
   {operation: reset, buttonLabel: "reset"},
   {operation: addOne, buttonLabel: "+"}
 ]
 const [ counter, setCounter ] = useState(0);
 const [ buttonContents,  ] = useState(initialState)
    return (
     <div className="App">
       <Header header="Simple React Counter"/>
       <Display counter={counter}/>
       {buttonContents.map(button => {
         return (
           <Button key={button.operation + button.buttonLabel} operation={button.operation} buttonLabel={button.buttonLabel} />
         )
       })}
     </div>
   );
}
Основная цель абстракции - сделать дочерние компоненты как можно более общими, чтобы их можно было использовать повторно в любом нужном вам виде. App-компонент должен содержать только ту информацию, которая специфична для приложения, и выводить или возвращать только более мелкие компоненты.

5. Соглашения об именовании

В React есть три основных соглашения об именовании, которые следует рассматривать как лучшие практики.
  1. Компоненты должны быть написаны в PascalCase - а также и названы по их основной функциональности, а не по специфике приложения (на случай, если вы измените ее позже).
  2. Элементы, которым нужны ключи, должны быть уникальными, неслучайными идентификаторами (например, отдельные карты или записи в карточной колоде или списке). Лучшая практика - не использовать для ключей только индексы. Допустимо назначение ключа, состоящего из конкатенации двух различных свойств объекта.
Основная цель ключа - хранить базовую информацию, чтобы React мог получить представление о том, что изменилось в приложении.
key={button.operation + button.buttonLabel}
  1. Методы должны быть в camelCase и называться по их назначению, а не быть специфичными для приложения. По тем же причинам, что и компоненты в PascalCase, методы должны быть названы по их назначению, а не по их особенности в приложении.

Итоги

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

Структура React приложения

2 года назад·6 мин. на чтение

Как структурировать React приложение и как организовать React приложение.

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

Технологии

Поскольку эта статья будет носить характер мнения. Сделаем несколько предположений о том, какие технологии будут использоваться в проекте: Нет четкого мнения по поводу стилей, идеальны ли Styled Components, CSS-модули или пользовательский набор для Sass, поэтому можно использовать любой подходящий вариант. Также предполагается, что тесты находятся рядом с кодом, а не в папке tests на верхнем уровне. Все, что здесь написано, может быть применимо, если вы используете обычный Redux, а не Redux Toolkit. Можно настроить ваш Redux как feature slices. Также будет удобно визуализировать созданные компоненты с помощью, например, Storybook. Также покажем, как проект будет выглядеть с этими файлами, если вы решите использовать его в своем проекте. Для примера будем использовать приложение “Библиотека”, который имеет страницу со списком книг, страницу со списком авторов и систему аутентификации.

Структура каталога

Структура каталогов верхнего уровня будет выглядеть следующим образом:
  • assets - глобальные статические файлы, такие как изображения, svg, логотип компании и т.д.
  • components - глобальные общие/повторно используемые компоненты, такие как макеты (обертки), компоненты форм, кнопки
  • services - JavaScript модули
  • store - глобальное хранилище Redux
  • utils - утилиты, помощники, константы и т.п.
  • views - можно также назвать pages, здесь будет содержаться большая часть приложения. Лучше сохранять привычные соглашения, где это возможно, поэтому src содержит все, index.js является точкой входа, а App.js устанавливает аутентификацию и маршрутизацию.
.
└── /src
    ├── /assets
    ├── /components
    ├── /services
    ├── /store
    ├── /utils
    ├── /views
    ├── index.js
    └── App.js
Также в проекте могут присутствовать некоторые дополнительные папки, которые у вас могут быть, такие как types, если это проект TypeScript, middlewares (промежуточное программное обеспечение), если необходимо, возможно, контекст для работы с Context API и т.д.

Псевдонимы (алиасы)

Полезно настроить систему на использование псевдонимов, чтобы все, что находится в папке компонентов, можно было импортировать как @components, изображения как @assets и т. д. Если у вас Webpack, это делается через конфигурацию resolve.
module.exports = {
  resolve: {
    extensions: ['js', 'ts'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@assets': path.resolve(__dirname, 'src/assets'),
      '@components': path.resolve(__dirname, 'src/components'),
      // ...etc
    },
  },
}
Это просто облегчает импорт из любого места в проекте и перемещение файлов без изменения импорта, и вы никогда не получите что-то вроде ../../../../../../../components/.

Компоненты

В папке components компоненты сгруппированы по типам - формы, таблицы, кнопки, макеты и т.д. Специфика зависит от конкретного приложения. В данном примере предполагается, что вы либо создаете собственную систему форм, либо создаете привязку к существующей системе форм (например, комбинируя Formik и Material UI). В этом случае вы создадите папку для каждого компонента (TextField, Select, Radio, Dropdown и т.д.), а внутри будет файл для самого компонента, стили, тесты и Storybook, если он используется.
  • Component.js - собственно компонент React
  • Component.styles.js - файл стилей для компонента
  • Component.test.js - тесты
  • Component.stories.js - файл Storybook.
Это имеет гораздо больше смысла, чем иметь одну папку, содержащую файлы для всех компонентов, одну папку, содержащую все тесты, одну папку, содержащую все файлы Storybook и т.д. Все связанное группируется вместе и легко находится.
.
└── /src
    └── /components
        ├── /forms
        │   ├── /TextField
        │   │   ├── TextField.js
        │   │   ├── TextField.styles.js
        │   │   ├── TextField.test.js
        │   │   └── TextField.stories.js
        │   ├── /Select
        │   │   ├── Select.js
        │   │   ├── Select.styles.js
        │   │   ├── Select.test.js
        │   │   └── Select.stories.js
        │   └── index.js
        ├── /routing
        │   └── /PrivateRoute
        │       ├── /PrivateRoute.js
        │       └── /PrivateRoute.test.js
        └── /layout
            └── /navigation
                └── /NavBar
                    ├── NavBar.js
                    ├── NavBar.styles.js
                    ├── NavBar.test.js
                    └── NavBar.stories.js
Обратите внимание, что в каталоге components/forms есть файл index.js. Часто справедливо советуют избегать использования файлов index.js, поскольку они не являются явными, но в данном случае это имеет смысл - в конечном итоге он будет индексом всех форм и будет выглядеть примерно так:
// src/components/forms/index.js

import { TextField } from './TextField/TextField'
import { Select } from './Select/Select'
import { Radio } from './Radio/Radio'

export { TextField, Select, Radio }
Затем, когда вам понадобится использовать один или несколько компонентов, вы можете легко импортировать их все сразу.
import { TextField, Select, Radio } from '@components/forms'
Иногда лучше использовать этот подход, чем создавать index.js внутри каждой папки внутри forms, так что теперь у вас есть только один index.js, который фактически индексирует всю директорию, в отличие от десяти файлов index.js только для того, чтобы упростить импорт для каждого отдельного файла.

Сервисы

Каталог services менее важен, чем компоненты, но если вы создаете простой модуль JavaScript, который используется остальной частью приложения, он может быть полезен. Обычный пример - модуль LocalStorage, который может выглядеть следующим образом:
.
└── /src
    └── /services
        ├── /LocalStorage
        │   ├── LocalStorage.service.js
        │   └── LocalStorage.test.js
        └── index.js
Пример сервиса:
// src/services/LocalStorage/LocalStorage.service.js
export const LocalStorage = {
  get(key) {},
  set(key, value) {},
  remove(key) {},
  clear() {},
}
import { LocalStorage } from '@services'

LocalStorage.get('foo')

Хранилище (стор, store)

Глобальное хранилище данных будет содержаться в директории store - в данном случае Redux. Каждая функциональность будет иметь свою папку, в которой будет содержаться слайс Redux Toolkit, а также экшены и тесты. Эта настройка также может быть использована с обычным Redux, вы просто создадите файл .reducers.js и .actions.js вместо slice. Если вы используете саги, это может быть .saga.js вместо .actions.js для действий Redux Thunk.
.
└── /src
    ├── /store
    │   ├── /authentication
    │   │   ├── /authentication.slice.js
    │   │   ├── /authentication.actions.js
    │   │   └── /authentication.test.js
    │   ├── /authors
    │   │   ├── /authors.slice.js
    │   │   ├── /authors.actions.js
    │   │   └── /authors.test.js
    │   └── /books
    │       ├── /books.slice.js
    │       ├── /books.actions.js
    │       └── /books.test.js
    ├── rootReducer.js
    └── index.js
Вы также можете добавить что-то вроде ui секции стора для обработки модальных окон, всплывающих уведомлений, переключения боковой панели и других глобальных состояний пользовательского интерфейса, что, лучше, чем иметь const [isOpen, setIsOpen] = useState(false) повсюду. В rootReducer вы импортируете все свои фрагменты и объединяете их с помощью combineReducers, а в index.js настраиваете магазин.

Утилиты (Utils)

Нужна ли вашему проекту папка utils - решать вам. Но обычно существуют некоторые глобальные функции, такие как валидация и конвертеры, которые могут легко использоваться в различных разделах приложения. Если вы упорядочите их - не будете иметь один файл helpers.js, содержащий тысячи функций, - она может стать полезным дополнением к организации вашего проекта.
.
└── src
    └── /utils
        ├── /constants
        │   └── countries.constants.js
        └── /helpers
            ├── validation.helpers.js
            ├── currency.helpers.js
            └── array.helpers.js
Опять же, папка utils может содержать все, что вы захотите, и что, по вашему мнению, имеет смысл держать на глобальном уровне. Если вам не нравятся "многоуровневые" имена файлов, вы можете просто назвать его validation.js. Однако, это облегчает навигацию по именам файлов при поиске в IDE.

Views

Здесь находится основная часть вашего приложения: в каталоге views. Любая страница в вашем приложении - это "представление" (view). В этом небольшом примере представления довольно хорошо согласуются со стором Redux, но не обязательно, что стор и представления будут полностью совпадать, поэтому они разделены. Кроме того, книги могут тянуть за собой авторов, и так далее. Все, что находится внутри представления, является сущностью, которая, скорее всего, будет использоваться только в этом конкретном представлении - BookForm, который будет использоваться только на маршруте /books, и AuthorBlurb, который будет использоваться только на маршруте /authors. Это могут быть специфические формы, модальные окна, кнопки, любые компоненты, которые не будут глобальными. Преимущество хранения всего в домене вместо объединения всех страниц в компоненты/страницы заключается в том, что это позволяет легко взглянуть на структуру приложения и узнать, сколько в нем представлений верхнего уровня, и понять, где находится все, что используется только этим представлением. Если есть вложенные маршруты, вы всегда можете добавить папку вложенных представлений в основной маршрут.
.
└── /src
    └── /views
        ├── /Authors
        │   ├── /AuthorsPage
        │   │   ├── AuthorsPage.js
        │   │   └── AuthorsPage.test.js
        │   └── /AuthorBlurb
        │       ├── /AuthorBlurb.js
        │       └── /AuthorBlurb.test.js
        ├── /Books
        │   ├── /BooksPage
        │   │   ├── BooksPage.js
        │   │   └── BooksPage.test.js
        │   └── /BookForm
        │       ├── /BookForm.js
        │       └── /BookForm.test.js
        └── /Login
            ├── LoginPage
            │   ├── LoginPage.styles.js
            │   ├── LoginPage.js
            │   └── LoginPage.test.js
            └── LoginForm
                ├── LoginForm.js
                └── LoginForm.test.js
Хранение всего в папках может показаться раздражающим, если вы никогда не создавали свой проект таким образом - вы всегда можете сделать его более плоским или перенести тесты в свой собственный каталог, имитирующий остальную часть приложения.

Итоги

Эта структура приложения и организация React, которая хорошо масштабируется для большого enterprise приложения, удобна для тестирования и стилизации, а также сохраняет все вместе в функционально ориентированном виде. Она более вложенная, чем традиционная структура, в которой все находится в компонентах и контейнерах. Легко взглянуть на эту систему и понять, что нужно для вашего приложения и куда идти, чтобы поработать над конкретным разделом или компонентом, который влияет на приложение глобально.