name: best-practice-reactjs description: Best practices, conventions, and patterns for this React + TypeScript + Redux Toolkit + MUI project
React + TypeScript + MUI Best Practices
This SKILL defines the rules, conventions, and patterns the AI must follow when working on this project.
1. Technology Stack
| Layer | Technology |
|---|---|
| Framework | React 18 (CRA - Create React App) |
| Language | TypeScript (strict mode, baseUrl: "src") |
| State | Redux Toolkit (createSlice, createAsyncThunk, RTK Query) |
| UI Library | MUI v5 (@mui/material, @emotion/react, @emotion/styled) |
| Theming | MUI CssVarsProvider + extendTheme (light/dark) |
| Routing | React Router v6 (createBrowserRouter, RouterProvider) |
| Forms | Formik + Yup |
| HTTP | RTK Query (fetchBaseQuery) + Axios (async thunks) |
| i18n | i18next + react-i18next (en / vi) |
| Date | Day.js |
| Icons | @iconify/react + @mui/icons-material |
| Formatting | Prettier |
2. Project Structure
src/
├── index.tsx # App entry point
├── index.css # Global styles
├── Routers.tsx # All route definitions
├── constant/ # App-wide constants
│ ├── index.ts # Exported constants (API_URL, token keys, etc.)
│ ├── endpoint.ts # API endpoint paths
│ ├── paths.ts # Route path constants
│ ├── enums.ts # Shared enums (ThemeMode, HttpStatusCode)
│ ├── type.ts # Shared TypeScript interfaces/types
│ └── regex.ts # Reusable regex patterns
├── contexts/ # React context providers
│ ├── AppProvider.tsx # Root provider (Redux + Theme + i18n)
│ └── ThemeProvider.tsx # MUI CssVarsProvider setup
├── store/ # Redux store
│ ├── configureStore.ts # Store config + RootState + AppDispatch exports
│ ├── customFetchBase.ts # RTK Query base query with auth headers
│ ├── hooks.ts # Typed useAppDispatch / useAppSelector
│ ├── app/ # App-global slice (auth, sidebar, profile)
│ │ ├── reducers.ts
│ │ ├── action.ts
│ │ └── selectors.ts
│ └── <domain>/ # Feature-specific slices
│ ├── reducers.ts
│ ├── <domain>ApiSlice.ts
│ └── selectors.ts
├── features/ # Feature pages/screens
│ ├── admin/ # Admin-side features
│ └── client/ # Client-side features
├── layouts/ # Layout components
│ ├── MainLayout.tsx # Client layout (Header + Outlet)
│ ├── AdminLayout.tsx # Admin layout (Drawer + Outlet)
│ ├── Header/ # Header components
│ └── sharedComponents/ # Reusable UI components
├── hooks/ # Custom React hooks
├── utils/ # Utility functions
│ ├── index.ts # General helpers (debounce, sleep, formatDate, getTheme)
│ ├── storage.ts # localStorage / sessionStorage wrappers
│ ├── colorSchemes.ts # MUI color scheme definitions
│ └── i18n.ts # i18next initialization
├── dictionaries/ # Translation files
│ ├── en/
│ ├── vi/
│ └── types/
└── icons/ # Custom icon components
Rules
- Absolute imports only — The
tsconfig.jsonsetsbaseUrl: "src", so always import from thesrcroot. Useimport { API_URL } from "constant"not"../../constant". - Feature-based organization — Each domain gets its own folder under
features/andstore/. - No business logic in components — Components only render UI. Business logic lives in
store/slices, actions, and selector hooks.
3. Redux Store Patterns
3.1 Store Configuration
The store is configured in store/configureStore.ts. All API slices are registered via a dynamic listApiSlices array:
import { configureStore, Middleware } from "@reduxjs/toolkit";
import { bannerApiSlice } from "./banner/bannerApiSlice";
const listApiSlices = [bannerApiSlice]; // ADD new API slices here
const apiReducer = listApiSlices.reduce(
(acc, slice) => ({
...acc,
[slice.reducerPath]: slice.reducer,
}),
{},
);
const apiMiddleware = listApiSlices.reduce<Middleware[]>((acc, slice) => {
return [...acc, slice.middleware];
}, []);
export const store = configureStore({
reducer: {
app: appReducer,
banner: bannerSlice,
...apiReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(...apiMiddleware),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
3.2 Typed Hooks
Always use the typed hooks from store/hooks.ts:
import { useAppDispatch, useAppSelector } from "store/hooks";
NEVER use raw useDispatch or useSelector from react-redux directly.
3.3 Slice Pattern (per domain)
Each domain folder under store/ contains three files:
reducers.ts — State shape + slice
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface BannerDataInterface {
id: number;
createdAt: string;
updatedAt: string;
fileName: string;
url: string;
}
export interface AdminBannerState {
isOpenAddingPopup: boolean;
bannerResPagination?: IResponsePagination<BannerDataInterface>;
pagination: PaginationInterface;
}
const initialState: AdminBannerState = {
isOpenAddingPopup: false,
pagination: { pageNum: 1, pageSize: 10 },
};
const bannerSlice = createSlice({
name: "banner",
initialState,
reducers: {
onToggleAddingBannerPopup: (state, action: PayloadAction<boolean>) => {
state.isOpenAddingPopup = action.payload;
},
onChangePaginationState: (
state,
action: PayloadAction<PaginationInterface>,
) => {
state.pagination = action.payload;
},
},
extraReducers: (builder) =>
builder.addMatcher(
bannerApiSlice.endpoints.getListBannerPagination.matchFulfilled,
(state, action) => {
state.bannerResPagination = action.payload;
},
),
});
<domain>ApiSlice.ts — RTK Query API
import { createApi } from "@reduxjs/toolkit/query/react";
import customFetchBase from "store/customFetchBase";
export const bannerApiSlice = createApi({
reducerPath: "bannerApi",
baseQuery: customFetchBase,
tagTypes: ["Banners"],
endpoints: (builder) => ({
getListBanners: builder.query({
query: () => ({
url: Endpoint.BANNER,
params: { pageNum: 1, pageSize: 10 },
}),
providesTags: ["Banners"],
}),
addBanner: builder.mutation({
query: (banner) => ({
url: Endpoint.BANNER,
method: "POST",
body: banner,
}),
}),
}),
});
export const { useGetListBannersQuery, useAddBannerMutation } = bannerApiSlice;
Rules:
- Always use
customFetchBaseas thebaseQuery— it injects the Bearer token automatically. - Use
tagTypesandprovidesTags/invalidatesTagsfor cache management. - Export auto-generated hooks.
selectors.ts — Domain-specific custom hooks
This is the selector hook pattern — the primary way components access store data:
export const useAdminBanner = () => {
const { isOpenAddingPopup, pagination } = useAppSelector<AdminBannerState>(
(state) => state.banner,
shallowEqual,
);
const dispatch = useAppDispatch();
const [addBanner] = useAddBannerMutation();
const onAddingBanner = useCallback(async (file: any) => {
try {
await addBanner(file);
dispatch(onToggleAddingBannerPopup(false));
refetch();
} catch (e) {}
}, [dispatch, refetch, addBanner]);
return { isOpenAddingPopup, onAddingBanner, ... };
};
Rules:
- Always use
shallowEqualwhen selecting objects from the store to prevent unnecessary re-renders. - Wrap action dispatchers in
useCallbackand return them from the hook. - Components should only consume these hooks, never dispatch actions directly.
- This pattern keeps components thin and testable.
3.4 Async Thunks
For complex async logic beyond simple CRUD (auth, multi-step processes), use createAsyncThunk:
export const signin = createAsyncThunk(
"app/signin",
async (data: SigninData) => {
const response = await api.post(Endpoint.SIGNIN, data);
if (response?.status === HttpStatusCode.Ok) {
return response.data;
}
throw AN_ERROR_TRY_RELOAD_PAGE;
},
);
Handle thunk results in extraReducers:
extraReducers: (builder) =>
builder.addCase(signin.fulfilled, (state, action) => {
state.token = action.payload.accessToken;
});
4. Component Patterns
4.1 Naming
| Item | Convention |
|---|---|
| Components | PascalCase, .tsx extension |
| Hooks | use prefix, camelCase, .ts or .tsx extension |
| Constants | UPPER_SNAKE_CASE |
| Types / Interfaces | PascalCase, Interface or Props suffix |
| Enums | PascalCase name, UPPER_SNAKE_CASE or PascalCase values |
| Slices | camelCase, file named reducers.ts |
| Selectors | use prefix hooks, file named selectors.ts |
4.2 Component Structure
import { memo } from "react";
import { Stack, Typography } from "@mui/material";
import { useAdminBanner } from "store/banner/selectors";
interface MyComponentProps {
title: string;
}
const MyComponent = ({ title }: MyComponentProps) => {
const { isOpenAddingPopup, onToggleOpenBanner } = useAdminBanner();
return (
<Stack>
<Typography variant="h6">{title}</Typography>
</Stack>
);
};
export default memo(MyComponent);
Rules:
- Use functional components exclusively — no class components.
- Use
memoon layout and list-item components to prevent unnecessary re-renders. - Destructure props in the function signature.
- Keep components focused — one component, one responsibility.
- Co-locate sub-components in the same feature directory.
4.3 Shared/Reusable Components
Place reusable UI components in src/layouts/sharedComponents/:
| Component | Purpose |
|---|---|
InputCustom |
Styled MUI TextField wrapper |
TableCustom |
Table with pagination |
DropZone |
File upload area (react-dropzone) |
Iconify |
Icon wrapper (@iconify/react) |
Loading |
Loading spinner |
Logo |
App logo |
Menu |
Navigation menu |
SideBar |
Sidebar navigation |
Wrapper |
Page content wrapper |
5. Routing
Use React Router v6 with createBrowserRouter:
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const listRouterPaths: RouteObject[] = [
// Public routes (no layout)
{ path: SIGNIN_PATH, element: <Signin /> },
// Client routes (MainLayout)
{
path: HOME_PATH,
element: <MainLayout />,
children: [{ path: "", element: <Home /> }],
},
// Admin routes (AdminLayout)
{
path: ADMIN_PATH,
element: <AdminLayout />,
children: [{ path: ADMIN_BANNER_PATH, element: <AdminBanner /> }],
},
];
Rules:
- Define all path constants in
constant/paths.ts. - Group routes by layout — public, client (MainLayout), admin (AdminLayout).
- Layouts use
<Outlet />for nested route rendering.
6. Theming
6.1 Color Scheme
Color schemes are defined in utils/colorSchemes.ts with full light/dark mode support. The theme is created via extendTheme and applied with CssVarsProvider:
const theme = extendTheme({ colorSchemes });
<CssVarsProvider theme={theme} defaultMode={DEFAULT_MODE}>
<CssBaseline />
{children}
</CssVarsProvider>
Rules:
- Never hardcode colors — always use
theme.palette.*or MUI'ssxprop with palette references. - Use
sxprop for component-level styling instead of creating separate CSS/styled components. - Extend the theme for custom palette keys (e.g.,
customColor,positive,purple).
6.2 Dark Mode
- Theme mode is persisted in localStorage under key
app_mode. - System preference detection is handled via
getThemeSystem(). - Toggle via
useColorScheme()from MUI.
7. Forms & Validation
Use Formik for form state management and Yup for schema validation:
import { useFormik } from "formik";
import * as yup from "yup";
const validationSchema = yup.object({
email: yup.string().email("Invalid email").required("Required"),
password: yup.string().min(6).required("Required"),
});
const formik = useFormik({
initialValues: { email: "", password: "" },
validationSchema,
onSubmit: (values) => {
/* handle */
},
});
Rules:
- Use
InputCustomcomponent from shared components for consistent form field styling. - Use regex patterns from
constant/regex.tsfor custom validations.
8. Internationalization (i18n)
Setup
Configured in utils/i18n.ts with dictionaries under dictionaries/{en,vi}/:
i18n.use(initReactI18next).init({
resources,
lng: clientStorage.get(LANGUAGE_STORAGE_KEY) || "en",
interpolation: { escapeValue: false },
});
Usage
import { useTranslation } from "react-i18next";
const { t } = useTranslation();
<Typography>{t("common.welcome")}</Typography>
Rules:
- Never hardcode user-facing strings — always use translation keys.
- Language preference is stored in localStorage under key
language. - Add translations in both
en/andvi/directories. - Type-safe keys are defined in
dictionaries/types/.
9. API Communication
9.1 Custom Fetch Base (RTK Query)
store/customFetchBase.ts automatically injects Bearer tokens:
const customFetchBase = fetchBaseQuery({
baseUrl: API_URL,
prepareHeaders: (headers) => {
const accessToken = clientStorage.get(ACCESS_TOKEN_STORAGE_KEY);
if (accessToken) {
headers.set("Authorization", `Bearer ${accessToken}`);
}
return headers;
},
});
9.2 API Endpoints
All endpoint paths are centralized in constant/endpoint.ts:
export const Endpoint = {
REFRESH_TOKEN: "",
GET_PROFILE: "/GET_PROFILE",
BANNER: "/banner",
};
Rules:
- All API endpoints must be added to
constant/endpoint.ts. - Use RTK Query (
createApi) for standard CRUD operations. - Use
createAsyncThunkonly for complex flows (auth, multi-step operations). - Environment variables prefixed with
REACT_APP_for API URLs.
10. Storage
Use the storage wrappers from utils/storage.ts:
import { clientStorage, sessionStorage } from "utils/storage";
// localStorage
clientStorage.set("key", value);
clientStorage.get("key");
clientStorage.remove("key");
// sessionStorage
sessionStorage.set("key", value);
sessionStorage.get("key");
Rules:
- Never access
localStorageorsessionStoragedirectly. - Always use
clientStorageorsessionStoragewrappers. - Values are automatically JSON serialized/deserialized.
11. Custom Hooks
Place reusable hooks in src/hooks/:
| Hook | Purpose |
|---|---|
useBreakpoint |
Responsive breakpoint detection |
useTheme |
Theme mode access |
useToggle |
Boolean toggle state |
useTranslate |
Translation shorthand |
useWindowSize |
Window dimensions tracking |
Rules:
- Prefix with
use— always follow the React hooks convention. - Keep hooks pure and focused — one concern per hook.
- Place domain-specific hooks in
store/<domain>/selectors.ts, not inhooks/. - Place generic, reusable hooks in
src/hooks/.
12. Type Safety
Shared Types (constant/type.ts)
export interface IResponse<T> {
data: T[];
code: string;
}
export interface IResponsePagination<T> extends IResponse<T> {
pagination: IPagination;
}
export interface PaginationInterface {
pageNum: number;
pageSize: number;
}
Rules:
- Define shared interfaces in
constant/type.ts. - Define domain interfaces in the corresponding
store/<domain>/reducers.ts. - Always type function params, return types, and state shapes.
- Use
RootStateandAppDispatchtypes fromstore/configureStore.ts. - Prefer interfaces over type aliases for object shapes.
- Use enums from
constant/enums.tsfor fixed value sets.
13. Layout System
Client Layout (MainLayout)
┌──────────────────────────────┐
│ Header │
├──────────────────────────────┤
│ │
│ <Outlet /> (content) │
│ │
└──────────────────────────────┘
Admin Layout (AdminLayout)
┌─────────┬────────────────────┐
│ │ │
│ Drawer │ <Outlet /> │
│ (240px) │ (content) │
│ │ │
└─────────┴────────────────────┘
Rules:
- Use MUI
Stackfor layout composition. - Layouts always render
<Outlet />for nested routes. - Memoize layout components with
memo().
14. Error Handling
// Use the constant for generic errors
import { AN_ERROR_TRY_RELOAD_PAGE } from "constant";
// In async thunks
try {
const response = await api.get(url);
if (response?.status === HttpStatusCode.Ok) return response.data;
throw AN_ERROR_TRY_RELOAD_PAGE;
} catch (e) {
throw e;
}
Rules:
- Use try/catch in all async operations.
- Use the
AN_ERROR_TRY_RELOAD_PAGEconstant as the generic error message key. - Use i18n keys for user-facing error messages.
- Never swallow errors silently — at minimum, log them.
15. Adding a New Feature Checklist
When adding a new feature (e.g., "Product"), follow this checklist:
Constants
- Add API endpoint to
constant/endpoint.ts - Add route path to
constant/paths.ts - Add any new enums to
constant/enums.ts - Add shared types to
constant/type.ts
- Add API endpoint to
Store
- Create
store/product/reducers.ts— state interface + slice - Create
store/product/productApiSlice.ts— RTK Query API - Create
store/product/selectors.ts— selector hooks - Register API slice in
store/configureStore.ts(listApiSlicesarray) - Register reducer in
store/configureStore.ts(reducerobject)
- Create
Feature UI
- Create page component in
features/client/product/orfeatures/admin/product/ - Use selector hooks from
selectors.tsfor state access - Use shared components from
layouts/sharedComponents/
- Create page component in
Routing
- Add route to
Routers.tsxunder the appropriate layout
- Add route to
i18n
- Add translation keys to both
dictionaries/en/anddictionaries/vi/
- Add translation keys to both
16. Environment Variables
All env vars are prefixed with REACT_APP_:
REACT_APP_API_URL=https://api.example.com
REACT_APP_HOT_LINE=1900xxxx
Rules:
- Define env vars in
.env(gitignored) and.env.example(committed). - Access via
process.env.REACT_APP_*. - Cast to
stringwhen assigning to constants.
17. Code Style
- Prettier is configured (
.prettierrc) — run before committing. - ESLint extends
react-appandreact-app/jest. - No implicit any is disabled (
noImplicitAny: false) but strive for full typing. - Strict mode is enabled in TypeScript.
- Use arrow functions for components and callbacks.
- Use
constby default;letonly when mutation is needed.
18. Testing
- Use React Testing Library (
@testing-library/react,@testing-library/jest-dom). - Test files co-located with components:
ComponentName.test.tsx. - Focus on user behavior, not implementation details.
- Mock Redux store using
@reduxjs/toolkittest utilities.
19. Performance Optimization
memo()on layout components and list items.useCallbackfor event handlers passed as props.useMemofor expensive computations.shallowEqualinuseAppSelectorwhen selecting objects.- Code-split routes with
React.lazy+Suspensefor large feature modules. - RTK Query caching — leverage
providesTags/invalidatesTags.
20. Common Anti-Patterns to AVOID
| ❌ Don't | ✅ Do |
|---|---|
Use raw useDispatch / useSelector |
Use useAppDispatch / useAppSelector |
Access localStorage directly |
Use clientStorage from utils/storage.ts |
| Hardcode strings in UI | Use t("key") from i18next |
| Hardcode API URLs | Use Endpoint from constant/endpoint.ts |
| Hardcode route paths | Use path constants from constant/paths.ts |
| Hardcode colors | Use MUI theme palette |
| Put business logic in components | Put it in store slices / selector hooks |
Use relative imports like ../../ |
Use absolute imports from src root |
| Dispatch actions directly in components | Use selector hooks from selectors.ts |
| Create standalone CSS files per component | Use MUI sx prop |