Доброго %время_суток, хабровчане!
Какие-либо статьи я еще не писал, опыта в этом нет от слова совсем. Поэтому, пожалуйста, не судите строго и прошу заранее прощения, если где-то совершу ошибки.
Это для меня совершенно новый опыт, мне никогда еще не доводилось делиться своим опытом и наработками с сообществом.
Предисловие
Когда я только начал знакомиться с React, мне приходилось искать, и искать довольно-таки долго, информацию, которая помогла бы мне грамотно организовать структуру моих будущих проектов, но дельной информации было мало.
Кто-то советовал делать плоскую структуру, в том числе и для папки, в которой находятся компоненты, кто-то же советовал просто разделять компоненты на "Молекулы", "Организмы" и т.д., но при этом, почти все, мало внимания уделяли сервисам, хукам, а именно тому, что содержало логику, не говорили о том, где лучше и как лучше их хранить.
НО, были и хорошие, одной из которых я вдохновился. В ней говорилось о том, что всю логику хранить в папке "Core". Ссылку на эту статью, на момент публикации, к сожалению, найти не могу, но как найду - обязательно прикреплю, либо отредактирую пост (если такое будет возможно, то на месте этого абзаца будет ссылка на статью), либо оставлю ссылку в виде комментария.
(!) Прошу вас, на время прочтения этой статьи, держать в голове мысль, что то, что тут предлагаю - это всего-лишь одна из идей того, как можно организовать структуру своего приложения. Не стоит воспринимать мои слова, как догму (это относится к тем, кто только начал свое знакомство с React), ведь я и сам новичок.
Буду очень рад, если вы оставите предложения по улучшению данной структуры, как и вашей конструктивной критике.
Components
Начну, пожалуй, с компонентов.
Компоненты, в данной структуре, разделяются на:
Умные (Smart)
Обычные (Ordinary)
Простые (Simple)
UI (UI, как ни странно)
Контейнеры (Containers)
Страницы (Pages)
Первые четыре группы (Smart, Ordinary, Simple и UI) хранятся в папке Components.
Поговорим немного о них:
UI компоненты - это те компоненты, которые заменяют нативные (стандартные) компоненты по типу: button, input, textarea, select и так далее.
Данные компоненты не могут использовать свое локальное хранилище и обращаться к глобальному.
Simple компоненты - это те компоненты, которые являются простыми, иначе говоря компоненты, в которых нет какой-либо логики, которые просто что-то рендерят.
Не могут использовать локальное хранилище и обращаться к глобальному.
Не могут использовать хуки, кроме тех, что изначально поставляются с React (за исключением useState).
Могут использовать в своей реализации UI компоненты.
Ordinary компоненты - это те компоненты, которые могут иметь какую-то логику, для отображения чего-либо.
Не могу использовать локальное хранилище, как и обращаться к глобальному.
Не могут использовать хуки, кроме тех, что изначально поставляются с React (за исключением useState).
Могут использовать в своей реализации Simple и UI компоненты.
Smart компоненты - это те компоненты, которые могут использовать относительно серьезную логику, для отображения чего-либо
Могут использовать локальное хранилище, как и обращаться к глобальному (не изменяя его)
Могут использовать все доступные хуки, кроме тех, что взаимодействуют с сетью
Могут использовать в своей реализации Ordinary, Simple и UI компоненты.
Структура папки Componets:
.
L-- src/
+-- components/
¦ +-- ordinary
¦ +-- simple
¦ +-- smart
¦ L-- ui
L-- ...
Оставшиеся две группы (Containers и Pages) имеют отдельные папки в корне приложения (папка src).
Containers - это те компоненты, которые формируют некие контейнеры, которые, в дальнейшем, используются для формирования страниц, которые, к тому же, содержать в своей реализации компоненты остальных четырех групп и при этом взаимодействовать, каким-либо образом, с сервисами или сетью, если говорить обобщенно.
Pages - это те компоненты, которые формируются благодаря контейнерам и компонентам из папки Components, если в этом есть необходимость. Могут, как и контейнеры, взаимодействовать с сервисами.
Структура корневой папки:
.
L-- src/
+-- components/
¦ +-- ordinary
¦ +-- simple
¦ +-- smart
¦ L-- ui
+-- containers
+-- pages
L-- ...
Сами компоненты должны иметь отдельные папки, есть 2 (это число не является константой) файла:
index.tsx - файл, в котором находится сам компонент
styled.ts - файл, в котором находятся стилизованные компоненты (его спокойно можно заменить на styles.sсss, либо же styles.css, в зависимости от того, чем вы пользуетесь для стилизации своих компонентов)
Пример компонента Align. Хотелось бы сказать, что этот компонент попадает под группу "Simple", так как он является глупым (не имеет нужды в локальном хранилище) и не заменяет никакой нативный, браузерный, UI компонент.
// index.tsx
import React, { memo } from "react";
import * as S from "./styled"; // Импортируем стилизованные компоненты
const Align = memo(({ children, axis, isAdaptable = false }: Readonly<Props>) => {
return (
<S.Align $axis={axis} $isAdaptable={isAdaptable}>
{children}
</S.Align>
);
});
export { Align };
export interface Props {
axis: S.Axis;
children?: React.ReactNode;
isAdaptable?: boolean;
}
// styled.ts
import styled, { css } from "styled-components";
const notAdaptableMixin = css`
width: 100%;
height: 100%;
max-height: 100%;
max-width: 100%;
`;
const adaptableMixin = css<AlignProps>`
width: ${(props) => !props.$axis.includes("x") && "100%"};
height: ${(props) => !props.$axis.includes("y") && "100%"};
min-width: ${(props) => props.$axis.includes("x") && "100%"};
min-height: ${(props) => props.$axis.includes("y") && "100%"};
`;
export const Align = styled.div<AlignProps>`
display: flex;
flex-grow: 1;
justify-content: ${(props) => (props.$axis.includes("x") ? "center" : "start")};
align-items: ${(props) => (props.$axis.includes("y") ? "center" : "start")};
${(props) => (props.$isAdaptable ? adaptableMixin : notAdaptableMixin)};
`;
export interface AlignProps {
$axis: Axis;
$isAdaptable?: boolean;
}
export type Axis = ("y" | "x")[] | "x" | "y";
Теперь, поговорим о самом сладком...
Core
Данная папка является "ядром" вашего приложения. В ней хранится все, для взаимодействия с сервером, глобальное хранилище, тема вашего приложения и т.д.
Эта папка содержит:
Config - в данной папке хранятся конфигурационные файлы приложения (например в ней можно хранить данные, необходимы для взаимодействия с бэкендом)
Constants - в данной папке находятся все константы, что используются в приложении (например сообщения об ошибках и предупреждениях)
Hooks - в данной папке хранятся все хуки кастомные хуки (хуки, что были сделаны вами).
Models - в данной папке хранятся модели, что приходят с бэкенда.
Schemes - в данной папке хранятся схемы форм, таблиц и т.д.
Services - в данной папке хранятся сами сервисы, благодаря которым и происходит общение с бэкендом.
Store - в данной папке хранятся схемы глобального хранилища (если Вы используете MobX), если же вы отдаете предпочтение Redux, то в данной папке могут хранится экшены, редьюсеры и т.д.
Theme (для Styled-Components) - в данной папке хранятся темы приложения.
Types - в данной папке хранятся вспомогательные типы, а также декларации модулей.
Utils - в данной папке хранятся вспомогательные, простые, функции, которые могут использоваться в хуках, либо же компонентах.
api.ts - в данном файле находится экземпляр HTTP клиента (например axios), который используют сервисы и который какой-то мутирует данные запросы (для передачи каких-либо заголовков, например).
Примеры содержимого папок
// config/api.config.ts
export const serverURI = "http://localhost:8080";
export const routesPrefix = '/api/v1';
// config/routes.config.ts
import { routesPrefix } from "./api.config";
export const productBrowserRoutes = {
getOne: (to: string = ":code") => `/product/${to}`,
search: (param: string = ":search") => `/search/${param}`,
};
export const productAPIRoutes = {
getOne: (code: string) => `${routesPrefix}/product/code/${code}`,
search: () => `${routesPrefix}/product/search`,
};
Довольно-таки удобно хранить все роуты в одном файле. Если вдруг со стороны бэкенда изменятся роуты, то их легко можно будет изменить в одном файле, не проходясь, при этом, по всему проекту.
// constants/message.constants.ts
export const UNKNOWN_ERROR = "Неизвестная ошибка";
// hooks/useAPI.ts
// Хук для взаимодействия с сервисами
/* eslint-disable react-hooks/exhaustive-deps */
import { useCallback, useEffect } from "react";
import { useLocalObservable } from "mobx-react-lite";
import type { API, Schema, Take } from "@core/types";
function useAPI<
F extends API.Service.Function<API.Response<any>>,
R extends Take.FromServiceFunction.Response<F>,
P extends Parameters<F>
>(service: F, { isPendingAfterMount = false, isIgnoreHTTPErrors = false }: Options = {}) {
const localStore = useLocalObservable<Store>(() => ({
isPending: {
value: isPendingAfterMount,
set: function (value) {
this.value = value;
},
},
}));
const call = useCallback(
async (...params: P): Promise<R["result"]> => {
localStore.isPending.set(true);
try {
const { data } = await service(...params);
const { result } = data;
localStore.isPending.set(false);
return result;
} catch (error) {
if (isIgnoreHTTPErrors === false) {
console.error(error);
}
localStore.isPending.set(false);
throw error;
}
},
[service, isIgnoreHTTPErrors]
);
const isPending = useCallback(() => {
return localStore.isPending.value;
}, []);
useEffect(() => {
localStore.isPending.set(isPendingAfterMount);
}, [isPendingAfterMount]);
return {
call,
isPending,
};
}
export { useAPI };
export interface Options {
isPendingAfterMount?: boolean;
isIgnoreHTTPErrors?: boolean;
}
type Store = Schema.Store<{ isPending: boolean }>;
// models/product.model.ts
// Описание модели товара
export interface ProductModel {
id: number;
name: string;
code: string;
info: {
description: string;
note: string;
};
config: {
isAllowedForPurchaseIfInStockZero: boolean;
isInStock: boolean;
};
seo: {
title: string;
keywords: string;
description: string;
};
}
// services/product.service.ts
// Сервисы для взаимодействия с товарами
import { api } from "../api";
import { routesConfig } from "../config";
import type { ProductModel } from "../models";
import type { API } from "../types";
export function getOne(code: string) {
return api.get<API.Service.Response.GetOne<ProductModel>>(
routesConfig.productAPIRoutes.getOne(code)
);
}
// theme/index.ts
// Тема приложения
import { DefaultTheme } from "styled-components";
export const theme: DefaultTheme = {
colors: {
primary: "#2648f1",
intense: "#151e27",
green: "#53d769",
grey: "#626b73",
red: "#f73d34",
orange: "#fdb549",
yellow: "#ffe243",
white: "white",
},
};
// types/index.tsx
// Вспомогательные типы
import type { AxiosResponse } from "axios";
export namespace API {
export namespace Service {
export namespace Response {
export type Upsert<T> = Response<T | null>;
export type GetOne<T> = Response<T | null>;
export type GetMany<T> = Response<{
rows: T[];
totalRowCount: number;
totalPageCount: number;
}>;
}
export type Function<T extends API.Response<any>, U extends any[] = any[]> = (
...params: U
) => Promise<AxiosResponse<T>>;
}
export type Response<T> = {
status: number;
result: T;
};
}
// utils/throttle.ts
function throttle<P extends any[]>(func: (...params: P) => any, limit: number) {
let inThrottle: boolean;
return function (...params: P): any {
if (!inThrottle) {
inThrottle = true;
func(...params);
setTimeout(() => (inThrottle = false), limit);
}
};
}
export { throttle };
// store/index.tsx
import { createContext } from "react";
import { useLocalObservable } from "mobx-react-lite";
import { app, App } from "./segments/app";
import { layout, Layout } from "./segments/layout";
import { counters, Counters } from "./segments/counters";
export const combinedStore = { layout, app, counters };
export const storeContext = createContext<StoreContext>(combinedStore);
export function StoreProvider({ children }: { children: React.ReactNode }) {
const store = useLocalObservable(() => combinedStore);
return <storeContext.Provider value={store}>{children}</storeContext.Provider>;
}
export type StoreContext = {
app: App;
layout: Layout;
counters: Counters;
};
// api.ts
// Экземпляр AXIOS для взаимодействия с сервером
import axios from "axios";
import { apiConfig } from "./config";
const api = axios.create({
baseURL: apiConfig.serverURI,
});
api.interceptors.request.use((req) => {
return {
...req,
baseURL: apiConfig.serverURI,
};
});
export { api };
Ух ты! Как же много получилось.
И напоследок...
Есть еще несколько, немаловажных папок, которые также следует упомянуть:
Assets - в данной папке хранятся все статичные файлы, такие как: иконки, изображения, шрифты и т.д. (их, конечно же, также стоит группировать и разделять на папки)
Routes - в данной папке (либо же файле, кому как больше нравится) хранятся все роуты приложения (пример будет ниже).
Styles - в данной папке хранятся все глобальные стили, которые применяются ко всем элементам и документу, в том числе.
// routes/index.tsx
import { Switch, Route } from "react-router-dom";
// Экспортируем страницы
import { Product } from "../pages/Product";
...
import { NotFound } from "../pages/NotFound";
import { routesConfig } from "../core/config";
const Routes = () => {
return (
<Switch>
<Route exact path={routesConfig.productBrowserRoutes.getOne()}>
<Product />
</Route>
{/* Объявляем как-то роуты */}
<Route>
<NotFound />
</Route>
</Switch>
);
};
export { Routes };
Остается еще 2 файла:
app.tsx - компонент приложения
Примерно так он может выглядеть:
// app.tsx
import React, { useEffect } from "react";
// Импортирует роуты
import { Routes } from "./routes";
const App = () => {
return (
<Routes />
);
};
export { App };
index.tsx - входной файл вашего приложения
Он же может выглядеть примерно так:
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { ThemeProvider } from "styled-components";
// импортируем наше
import { App } from "./app";
// импортируем глобальные стили
import { BodyStyles } from "./styles";
import { StoreProvider } from "../core/store";
// импортируем тему
import { theme } from "../core/theme";
import reportWebVitals from "./reportWebVitals";
const app = document.getElementById("app");
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<BodyStyles />
<BrowserRouter>
<StoreProvider>
<App />
</StoreProvider>
</BrowserRouter>
</ThemeProvider>
</React.StrictMode>,
app
);
reportWebVitals();
И на этом, я думаю, стоит закончить.
Итоговая структура выглядит вот так:
.
L-- src/
+-- assets/
¦ +-- fonts
¦ L-- icons
+-- components/
¦ +-- ordinary
¦ +-- simple
¦ +-- smart
¦ L-- ui
+-- containers
+-- core/
¦ +-- config
¦ +-- constants
¦ +-- hooks
¦ +-- models
¦ +-- schemes
¦ +-- services
¦ +-- store
¦ +-- theme
¦ +-- types
¦ +-- utils
¦ L-- api.ts
+-- pages
+-- routes
+-- styles
+-- app.tsx
L-- index.tsx
Заключение
Если вам понравилась эта статья и вы узнали что-то интересное для себя, то я очень этому рад.
Ссылка на репозиторий (за такой скудный ридми, мне еще не по силам разговорный английский).
Всем удачи и огромное спасибо за внимание.