Good% time_day, khabrovites!
I have not written any articles yet, there is no experience in this from the word at all. Therefore, please do not judge strictly and I apologize in advance if I make mistakes somewhere.
This is a completely new experience for me, I have never had the opportunity to share my experience and best practices with the community.
Foreword
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:
. βββ src/ βββ components/ β βββ ordinary β βββ simple β βββ smart β βββ ui βββ ...
(Containers Pages) ( src).
Containers - , , , , , , , , - , , .
Pages - , Components, . , , .
:
. βββ src/ βββ components/ β βββ ordinary β βββ simple β βββ smart β βββ ui βββ containers βββ pages βββ ...
, 2 ( ) :
index.tsx - ,
styled.ts - , ( styles.sss, 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();
, , .
:
.
βββ src/
βββ assets/
β βββ fonts
β βββ icons
βββ components/
β βββ ordinary
β βββ simple
β βββ smart
β βββ ui
βββ containers
βββ core/
β βββ config
β βββ constants
β βββ hooks
β βββ models
β βββ schemes
β βββ services
β βββ store
β βββ theme
β βββ types
β βββ utils
β βββ api.ts
βββ pages
βββ routes
βββ styles
βββ app.tsx
βββ index.tsx
- , .
.