A collection of React + TypeScript + MUI starter projects. The recommended
flavor is with-mui/typescript-v3 — the most complete baseline with
Redux Toolkit, React Query, role-based routing, Formik/Yup, theming, and
ready-made window.flash / window.modal notifications.
The other versions (
typescript/,typescript-v2/) are kept for history. New work should start from v3.
cd with-mui/typescript-v3
npm install # if peer-dep errors, use: npm install --legacy-peer-deps
npm startOpen http://localhost:3000. The CRA dev server, ESLint config, and all boilerplate wiring (providers, theme, auth bootstrap) are already in place.
| Folder | Status | One-liner |
|---|---|---|
with-mui/typescript/ |
v1 (legacy) | Basic CRA + TypeScript + MUI. Uses CRACO for src/* path alias. |
with-mui/typescript-v2/ |
v2 (legacy) | TS + MUI + enhancements over v1. |
with-mui/typescript-v3/ |
v3 (recommended) | Full app shape: routing, auth, RBAC, theming, forms, utilities. |
Reach for typescript-v3 when you want a production-shaped React 18 +
TypeScript SPA in minutes, without re-deciding the boring parts:
- UI: MUI 5 with a typed custom theme (
colors,header,componentCustomStyles) and a light-theme variant ready to fork. - State: Redux Toolkit with typed
useSelector/useDispatchplususeActions()for ergonomic action creators. - Server state: React Query with a
useQueryStatewrapper that exposes a flatfoundErrorfor cleaner conditionals. - Routing: react-router 6 with a split-by-role definition
(
auth/admin/public) and three ready-made route guards (Authenticated,Guest,Public). - Forms: Formik + Yup, with custom Yup methods
(
phone,password,confirmPassword,onlyAlphabets,alphaNumeric,length,minLength,maxLength). - Notifications:
window.flash({ message, variant })andwindow.modal({ type, ... })— global, no prop drilling. - Auth:
useAuth()hook (login/logout/initialize), redux-backed session, role-based access on routes. - Utilities: cookie, route, axios, file, date, object, array, string,
number helpers — all re-exported from
src/utils.
reactjs-boilerplates/
├── README.md # this file
├── .gitignore
└── with-mui/
└── typescript-v3/ # ← the recommended boilerplate
├── package.json # scripts, deps
├── package-lock.json
├── tsconfig.json # baseUrl + paths (src/*, @/public/*)
├── .eslintrc.json # ESLint + TS + React rules
├── .eslintignore
├── .gitignore
├── public/ # static assets served at /
│ ├── index.html # <title>Ping</title>, root div
│ ├── manifest.json
│ ├── robots.txt
│ ├── favicon.ico
│ └── logo192.png / logo512.png
└── src/
├── App.tsx # provider tree (see "Wiring" below)
├── index.tsx # mounts <BrowserRouter><App /></BrowserRouter>
├── createEmotionCache.ts # emotion cache (key: "css")
├── react-app-env.d.ts
├── reportWebVitals.ts
│
├── api/ # API layer
│ ├── index.ts
│ ├── auth.ts # class AuthApi → /auth/* on axiosInstance
│ └── other-usables/
│ └── auth-api-to-handle-oauth-and-credentials-auth/
│ └── auth.ts # reference: adds oAuth() → /auth/social
│
├── assets/ # fonts, images, global SCSS
│ ├── index.tsx # re-exports AppLogoFullImage
│ ├── font/Gilroy/ # OTF family (Light/Regular/Medium/...)
│ ├── img/png/app-logo-full.png
│ └── scss/
│ ├── font.scss # @font-face for Gilroy
│ └── global.scss # body reset, .skeleton-box, scrollbar
│
├── components/ # UI building blocks
│ ├── index.tsx
│ ├── app-specific/ # tied to this app's pages
│ │ ├── app-loader.tsx
│ │ ├── card-spinner.tsx
│ │ ├── error-boundary.tsx
│ │ ├── error-ui.tsx
│ │ ├── page-not-found.tsx
│ │ └── redirect.tsx
│ └── common/ # project-agnostic primitives
│ ├── custom-button.tsx
│ ├── custom-card.tsx
│ ├── custom-link.tsx
│ ├── custom-text.tsx
│ ├── flash-message.tsx
│ ├── media-query-box.tsx
│ ├── aligned-boxes/ # FlexBox, FlexRow, GridBox,
│ │ # XCenter, YCenter, XYCenter, JustifyBetween
│ └── custom-modal/
│ ├── index.tsx # <CustomModal /> event subscriber
│ └── components/
│ └── confirmation-modal.tsx
│
├── content/ # page-level content per route group
│ ├── index.tsx # HomePageContent — role-based redirect
│ ├── admin/index.tsx # AdminHomeContent
│ ├── auth/
│ │ ├── index.tsx
│ │ ├── login.tsx # LoginPageContent
│ │ ├── components/
│ │ │ └── logo.tsx
│ │ └── other-usables/
│ │ └── o-auth.txt # reference: OAuthPageContent
│ └── public/
│ ├── index.tsx
│ └── example.tsx
│
├── data/ # static config used app-wide
│ ├── index.ts
│ ├── static.ts # authSetup, rbacSetup, projectSetup
│ └── other-usables/
│ ├── static.txt # extended setup w/ MSAL
│ └── o-auth-config.txt # GOOGLE/MICROSOFT env config
│
├── docs/ # in-repo notes
│ ├── setup.md
│ ├── tips.md
│ └── libraries/other.md
│
├── guard/ # route-level authorization wrappers
│ ├── authenticated.tsx # redirects unauthenticated → authPage
│ ├── guest.tsx # redirects authenticated → homePage
│ └── public.tsx # pass-through
│
├── hooks/ # reusable React hooks
│ ├── index.tsx
│ ├── useAuth.ts # wraps authApi + redux + cookies
│ ├── useActions.ts # bindActionCreators, typed
│ ├── useQueryState.ts # [data, isLoading, { foundError, ... }]
│ ├── useUniqueKey.ts # memoized uniqid() array for list keys
│ └── other-usables/
│ └── useAuth-with-oauth.txt # reference: OAuth variant
│
├── layouts/ # per-role layout shells
│ ├── auth/ # AuthLayout, layout-settings
│ └── admin/ # AdminLayout, layout-settings, header/
│
├── model/ # shared TS types
│ ├── index.tsx # AUTH_DATA, AUTH_STATE, etc.
│ └── custom-models.ts # type Overwrite<T, U>
│
├── provider/
│ └── auth-provider.tsx # calls useAuth().initialize() once
│
├── redux/ # Redux Toolkit
│ ├── index.ts # configureStore + typed hooks
│ ├── actions.ts
│ ├── reducer.ts # combineReducers({ auth, theme })
│ └── slices/
│ ├── auth.ts # initialize / login / logout
│ └── theme.ts # current theme name (localStorage)
│
├── routes/ # routing
│ ├── index.tsx # top-level routes + /404 + catch-all
│ ├── router.tsx # <Routes> = useRoutes(routes)
│ ├── definition/
│ │ ├── index.tsx # { auth, admin, public } map
│ │ ├── auth.tsx # /auth/login
│ │ ├── admin.tsx # /admin
│ │ ├── public.tsx # /public
│ │ └── other-usables/auth.txt # reference: signup + oauth
│ └── navigation-links/
│ ├── index.tsx
│ └── admin.tsx
│
├── schema/ # Yup schemas (one per feature)
│ └── index.ts # empty placeholder — add yours here
│
├── theme/ # MUI theming
│ ├── index.tsx
│ ├── ThemeProvider.tsx
│ ├── utils.ts # THEME_NAMES, themeCreator, types
│ ├── viewport.tsx # BREAKPOINTS, mediaQuery.up/down
│ ├── variants/
│ │ └── pure-light-theme.ts
│ └── icons/
│ └── google.tsx # <GoogleIcon />
│
└── utils/ # pure helpers (all re-exported)
├── index.ts # barrel
├── api-utils.ts # axiosInstance, getError, etc.
├── cookie-utils.ts # getCookie/setCookie/deleteCookie
├── route-utils.ts # isActiveRoute
├── window-utils.ts # EventEmitter + window.flash / .modal
├── library-utils.ts # yup custom methods + { yup, uniqId }
├── validation-utils.js # isRequiredField (Formik-aware)
├── object-utils.js # convertDropDownObject, dot-notation
├── array-utils.js # globalSearch, removeDuplicates, ...
├── string-utils.js # millify, plurify, isValidValue
├── number-utils.js # filterNumbers, numberWithCommas
├── date-utils.js # getDateCollapsed, isDateBefore, ...
└── file-utils.js # compressFile, getBase64, downloadLink
- React 18.2 + TypeScript 4.7
- MUI 5 (
@mui/material,@mui/lab,@mui/icons-material) + Emotion for styling - Redux Toolkit 1.8 + react-redux 8
- React Query 3.39
- react-router-dom 6.3
- Formik 2.2 + Yup 0.32 +
yup-phone - notistack 2 (snackbars), react-helmet 6 (document titles)
- axios 0.27 (with a configured instance and auth-header interceptor)
react-phone-number-input,date-fns,compress.js,pluralize,uniqid,validator,csstype,web-vitals- ESLint 8 with
@typescript-eslintandeslint-plugin-react - react-scripts 5 (CRA 5)
See with-mui/typescript-v3/package.json for exact versions and the full
dev list.
A typed theme object with colors, general, header, and a
componentCustomStyles map. Module-augmented into MUI's Theme and
ThemeOptions interfaces in src/theme/utils.ts. The active variant is
held in Redux and persisted to localStorage.theme; <ThemeProvider> in
src/theme/ThemeProvider.tsx reads it and passes the resolved
createTheme(...) to MUI.
A drop-in replacement for MUI's Typography that pulls its style from
theme.componentCustomStyles[variant]. Supports h1–h6, subtitle1/2,
body1/2, caption, button, overline, default p, and label
(renders as FormLabel). Colors are looked up by name from
theme.colors.
A Button with built-in loading state (CircularProgress start icon),
optional href (string for a plain link, or { to, options } for a
react-router navigate), and a linkStyle mode that turns it into a
text-only link.
Card with optional customHeader, cardActions, headerProps, and
loading (renders CardSpinner instead of children).
Zero-config flex/grid wrappers: FlexBox, FlexRow, GridBox,
XCenter, YCenter, XYCenter, JustifyBetween. Each accepts a
style override.
Wraps a div and injects a media-query rule via the mediaQuery.up()
or mediaQuery.down() helper from src/theme/viewport.tsx. Useful when
you need a one-off responsive tweak without leaving JSX.
A single <FlashMessage /> is mounted in App.tsx and listens to an
EventEmitter event. Anywhere in the app you can call:
window.flash({ message: "Saved", variant: "success" });
// variants: default | success | error | warning | infoWired in src/utils/window-utils.ts (createEventEmitters), consumed
in src/components/common/flash-message.tsx (notistack).
A single <CustomModal /> mounted in App.tsx listens to a second
EventEmitter event. Open any component as a modal, or use the built-in
confirmation dialog:
// custom component
window.modal({
type: "custom",
component: MyComponent,
containerProps: { closeOnClick: true },
contentContainerProps: { sx: { width: 480 } },
});
// confirmation
window.modal({
type: "confirmation",
title: "Delete this item?",
description: "This cannot be undone.",
onConfirm: async () => { await api.delete(id); },
onCancel: () => {},
});Wired in src/utils/window-utils.ts, consumed in
src/components/common/custom-modal/.
A single QueryClient is created in App.tsx. Prefer the local wrapper
for ergonomic destructuring:
const [data, isLoading, { foundError, refetch }] = useQueryState({
queryKey: ["users", page],
queryFn: () => api.getUsers(page),
});
// `foundError` is non-null only when the query is settled AND errored
// — no need to guard against transient errors during refetch.useSelector and useDispatch are re-typed in src/redux/index.ts and
re-exported from src/hooks/index.tsx. useActions() returns a flat
object of bound action creators:
const { authActions, theme } = useActions();
authActions.login(userData);
theme.changeTheme("pure-light-theme");serializableCheck is disabled in the store config to allow
non-serializable values in actions.
useAuth() exposes login, logout, initialize, plus the redux
auth slice (isAuthenticated, isInitialized, data).
const { login, logout, data, isAuthenticated } = useAuth();
await login({ email, password }); // sets cookie + redux
await logout(); // clears cookie + redux + reload
await initialize(); // call once on app bootThree route-level guards:
<Authenticated roles={["admin"]}>— redirects unauthenticated users toauthSetup.authPage?backToURL=<path>; redirects role mismatches to/404.<Guest>— redirects authenticated users toauthSetup.homePage(used for login/signup pages).<Public>— pass-through.
Static config (authSetup, rbacSetup, projectSetup) lives in
src/data/static.ts.
Yup is augmented with custom methods. Import { yup, uniqId } from
src/utils:
import { yup } from "src/utils";
const schema = yup.object({
email: yup.string().email().required(),
phone: yup.string().phone().required(), // react-phone-number-input
password: yup.string().password().required(), // 8+, upper/lower/digit/special
confirm: yup.string().confirmPassword("password").required(),
name: yup.string().onlyAlphabets().required(),
code: yup.string().alphaNumeric().required(),
pin: yup.number().length(6).required(),
});isRequiredField(yupSchema, "address.city") from
src/utils/validation-utils.js returns whether a (possibly nested)
field is .required() — useful for rendering a * next to a label.
Yup schemas go in src/schema/ (one file per feature; the directory is
intentionally empty so you can grow it as you add forms).
axiosInstance is created with a baseURL and a request interceptor
that attaches Authorization: Bearer <token> from the cookie named by
authSetup.tokenAccessor. A 401 response interceptor is left commented
out as a drop-in.
import { axiosInstance, createApiFunction, handleError } from "src/utils";
const data = await createApiFunction(() => axiosInstance.get("/users"));
// ↳ resolves with response.data; passes a thrown error through.
try { await api.something(); }
catch (err) { handleError(err); } // → window.flash({ variant: "error", ... })setCookie("token", value, { "max-age": 3600, secure: true });
const token = getCookie("token");
deleteCookie("token");A few highlights:
// dropdown <-> backend shape
import { convertDropDownObject } from "src/utils";
const option = convertDropDownObject({
value: user,
valueAccessor: "_id",
labelAccessor: "name",
});
// → { value: "...", label: "..." }
// dot-notation read/write
import { accessValueByDotNotation, modifyObjectByDotNotation } from "src/utils";
const city = accessValueByDotNotation(user, "address.city");
modifyObjectByDotNotation(user, "address.city", "Chennai");
// dedupe + validate arrays of objects
import { removeDuplicates, validateArrayOfObjects } from "src/utils";
const unique = removeDuplicates(rows, { keys: ["email"] });
await validateArrayOfObjects(rows, {
requiredFieldKeys: ["email"],
removeDuplicates: true,
duplicationOptions: { keys: ["email"], detectExactDuplicate: false },
validationTypes: { email: "email", mobile: "number" },
});
// number / string / date helpers
import {
numberWithCommas, filterNumbers, millify, plurify, isValidValue,
getDateCollapsed, isDateBefore, isDateAfter, isBetweenDate,
} from "src/utils";import { compressFile, getBase64, downloadLink } from "src/utils";
const out = await compressFile([file], { size: 2, quality: 0.75 }); // MB
const dataUrl = await getBase64(file);
downloadLink({ link: fileUrl, name: "report.pdf" });compressFile is browser-only (lazy require("compress.js")). It also
returns each entry with a dataWithPrefix (e.g. data:image/jpeg;base64,...)
for direct <img src> use.
Routes are defined per role in src/routes/definition/<role>.tsx and
combined in src/routes/definition/index.tsx. The top-level
src/routes/index.tsx assembles them with <Helmet> titles, guards,
layouts, and a /404 + catch-all * route.
To add a new admin page:
- Add the component in
src/content/admin/. - Append a
ROUTE_DEFINITIONtosrc/routes/definition/admin.tsx. - If it needs nav, add it to
src/routes/navigation-links/admin.tsx.
AuthLayout and AdminLayout are the two shells. AdminLayout already
embeds a CustomButton that calls useAuth().logout, hidden on the
signup route via isActiveRoute. Each layout has a sibling
layout-settings.tsx (e.g. header height) used by both the layout
itself and any inner content that needs to compensate for it.
<Redirect to="/login" />Renders null and calls useNavigate(to, { replace: true }) once on
mount. Useful for declarative redirects inside other components.
npm run lint # eslint src/**/*.tsx
npm run lint:fix # same, with --fixConfig extends eslint:recommended, plugin:react/recommended,
plugin:@typescript-eslint/recommended, and plugin:react/jsx-runtime.
Enforces double quotes and semicolons. .eslintignore covers
node_modules, dist, build.
What you want to change → where to change it.
| Want to change | Edit |
|---|---|
| App name / brand | projectSetup.title in src/data/static.ts, <title> in public/index.html, and the <Helmet> titles in src/routes/definition/*.tsx |
| App logo | Replace src/assets/img/png/app-logo-full.png (re-exported as AppLogoFullImage from src/assets/index.tsx) |
| API base URL | baseURL in src/utils/api-utils.ts — currently hard-coded. Recommended: move to process.env.REACT_APP_API_URL (create a .env file) or to projectSetup.baseURL |
| Default theme | projectSetup.defaultTheme in src/data/static.ts; the literal in src/redux/slices/theme.ts (TODO) should also be updated |
| Theme tokens (colors, header, typography) | src/theme/variants/pure-light-theme.ts |
| Add a new theme variant | Create a file in src/theme/variants/, re-export from src/theme/variants/index.tsx, register it in the customThemes map and the THEME_NAMES enum in src/theme/utils.ts |
| Default phone country | projectSetup.defaultPhonenumberCountry in src/data/static.ts |
| Auth/login/signup pages, token cookie name | authSetup in src/data/static.ts |
| RBAC roles and per-role home page | rbacSetup in src/data/static.ts |
| Fonts | Drop new files into src/assets/font/<family>/, declare @font-face in src/assets/scss/font.scss, update body { font-family } in src/assets/scss/global.scss |
| Global SCSS / scrollbar / skeleton | src/assets/scss/global.scss |
| Add a new role | New src/routes/definition/<role>.tsx + entry in src/routes/definition/index.tsx + src/routes/navigation-links/<role>.tsx + src/layouts/<role>/ + a <Authenticated roles={...}> guard usage |
| Public favicon / PWA assets | public/favicon.ico, public/logo192.png, public/logo512.png, public/manifest.json |
| HTTP client error handling | src/utils/api-utils.ts (the 401 response interceptor is commented out) |
Several folders ship a sibling other-usables/ directory whose files
are *.txt (not .ts / .tsx) so TypeScript excludes them from
the build:
src/api/other-usables/auth-api-to-handle-oauth-and-credentials-auth/auth.tssrc/content/auth/other-usables/o-auth.txtsrc/data/other-usables/o-auth-config.txtsrc/data/other-usables/static.txtsrc/hooks/other-usables/useAuth-with-oauth.txtsrc/routes/definition/other-usables/auth.txt
These are reference implementations for features that are intentionally opt-in — most notably OAuth (Google / Microsoft) and sign-up. They are not imported anywhere.
To enable one of them:
- Open the
.txtfile. - Copy its contents into the matching live file (e.g. overwrite
src/api/auth.ts, or extendsrc/data/static.tswith the values fromstatic.txt). - Adjust as needed for your project.
Run from with-mui/typescript-v3/:
| Script | What it does |
|---|---|
npm start |
CRA dev server on http://localhost:3000 |
npm run build |
Production build to build/ |
npm test |
Jest via CRA (no extra tests are committed) |
npm run eject |
CRA eject (one-way) — you almost certainly do not want this |
npm run lint |
eslint src/**/*.tsx |
npm run lint:fix |
same, with --fix |
tsconfig.json declares:
{
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"],
"@/public/*": ["./public/*"]
}
}so every import in the boilerplate is written as
import { foo } from "src/...". TypeScript and your editor resolve
these aliases natively.
Runtime caveat:
react-scripts5 does not honortsconfig.pathsat build time without a custom config (e.g. CRACO). The v1 boilerplate (with-mui/typescript/) ships acraco.config.tsfor this. v3 does not include one yet — the existingtsconfig.jsoncovers editor / type-check time only. If you need runtime aliasing, add CRACO or an equivalent.
These are flagged in the code and called out so you can decide how to handle them:
axiosInstance.baseURLis hard-coded insrc/utils/api-utils.tsto a dev URL. Move it to an env var orprojectSetup.baseURLbefore shipping.- 401 response interceptor is commented out at the bottom of
src/utils/api-utils.ts— uncomment and adjust for your auth flow. theme.tsslice has aTODOabout a circular dependency withsrc/theme(it can't importTHEME_NAMEScleanly).tsconfigis loose:strict: falseandstrictNullChecks: false. Some legacy.jsfiles insrc/utils/rely on this (allowJs: true).src/index.tsxuses React 17ReactDOM.render. A commented React 18createRootblock sits above it with a note about a re-render quirk. Swap them when you're ready.src/data/static.tsdefaults still reference the dev URL (http://localhost:5000) and"IN"as the default phone country.