Building a React + MUI frontend for an ERP system with 100+ screens teaches you things no tutorial covers. Here are the patterns I wish I'd known from day one — all learned the hard way on AgriGen ERP.
The Shared Styles Problem
Early in the project, every component defined its own useStyles hook with duplicated values:
// BAD — repeated across 40 files
const useStyles = makeStyles({
tableHeader: {
backgroundColor: #98c379">"#1a237e",
color: #98c379">"#fff",
fontWeight: 700,
},
});
When we needed to change the header color, we had to hunt through 40 files.
The Fix: ModuleStyles.js Pattern
// styles/moduleStyles.js
import { makeStyles } from #98c379">"@material-ui/core/styles";
export const useTableStyles = makeStyles((theme) => ({
headerCell: {
backgroundColor: theme.palette.primary.dark,
color: #98c379">"#fff",
fontWeight: 700,
whiteSpace: #98c379">"nowrap",
},
row: {
#98c379">"&:hover": { backgroundColor: "rgba(14, 165, 233, 0.04)" },
cursor: #98c379">"pointer",
},
actionCell: {
width: 120,
textAlign: #98c379">"center",
},
}));
export const useFormStyles = makeStyles((theme) => ({
fieldGroup: {
display: #98c379">"grid",
gridTemplateColumns: #98c379">"repeat(auto-fit, minmax(240px, 1fr))",
gap: theme.spacing(2),
},
sectionLabel: {
color: theme.palette.text.secondary,
fontSize: #98c379">"0.75rem",
textTransform: #98c379">"uppercase",
letterSpacing: #98c379">"0.08em",
marginBottom: theme.spacing(1),
},
}));
Now every component imports from one place. One change, every screen updates.
Reusable Table Component
After building 15 data tables with the same MUI TableContainer boilerplate, we abstracted it:
// components/shared/DataTable.jsx
export function DataTable({ columns, rows, onRowClick, loading }) {
const classes = useTableStyles();
if (loading) return <TableSkeleton cols={columns.length} />;
return (
<TableContainer component={Paper} elevation={0} variant=#98c379">"outlined">
<Table size=#98c379">"small">
<TableHead>
<TableRow>
{columns.map((col) => (
<TableCell
key={col.field}
className={classes.headerCell}
align={col.align ?? #98c379">"left"}
style={{ width: col.width }}
>
{col.header}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, i) => (
<TableRow
key={row.id ?? i}
className={classes.row}
onClick={() => onRowClick?.(row)}
>
{columns.map((col) => (
<TableCell key={col.field} align={col.align ?? #98c379">"left"}>
{col.render ? col.render(row) : row[col.field]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
Usage becomes declarative:
<DataTable
columns={[
{ field: #98c379">"invoiceNo", header: "Invoice #", width: 120 },
{ field: #98c379">"date", header: "Date" },
{
field: #98c379">"amount",
header: #98c379">"Amount (Rs.)",
align: #98c379">"right",
render: (row) => row.amount.toLocaleString(),
},
{
field: #98c379">"actions",
header: #98c379">"",
render: (row) => <ActionMenu row={row} />,
},
]}
rows={invoices}
loading={isLoading}
onRowClick={(row) => navigate(`/invoices/${row.id}`)}
/>
New table screens take 10 minutes instead of an hour.
Connecting MUI inputs to react-hook-form cleanly:
// components/shared/FormTextField.jsx
export function FormTextField({ name, label, control, rules, ...props }) {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field, fieldState }) => (
<TextField
{...field}
{...props}
label={label}
error={!!fieldState.error}
helperText={fieldState.error?.message}
variant=#98c379">"outlined"
size=#98c379">"small"
fullWidth
/>
)}
/>
);
}
Then in any form:
<FormTextField
name=#98c379">"weight"
label=#98c379">"Weight (kg)"
control={control}
rules={{ required: #98c379">"Weight is required", min: { value: 0, message: "Must be positive" } }}
type=#98c379">"number"
/>
No boilerplate. Validation wired automatically.
In a screen with 200+ rows and real-time WebSocket updates, re-renders were killing performance.
useMemo for Expensive Computations
const totals = useMemo(() => {
return invoices.reduce(
(acc, inv) => ({
weight: acc.weight + inv.weightKg,
amount: acc.amount + inv.totalAmount,
count: acc.count + 1,
}),
{ weight: 0, amount: 0, count: 0 }
);
}, [invoices]);
React.memo for Pure Row Components
const InvoiceRow = React.memo(function InvoiceRow({ invoice, onEdit }) {
return (
<TableRow onClick={() => onEdit(invoice.id)}>
<TableCell>{invoice.invoiceNo}</TableCell>
<TableCell>{invoice.totalAmount.toLocaleString()}</TableCell>
</TableRow>
);
});
Without React.memo, updating one invoice caused every row to re-render. With it — only the changed row updates.
useCallback for Stable References
const handleEdit = useCallback(
(id) => {
navigate(`/invoices/${id}`);
},
[navigate]
);
Pass handleEdit to React.memo components — it won't trigger re-renders because the reference is stable.
MUI v4 Specific Tricks
1. Accessing theme in plain JS (not inside makeStyles):
import { createMuiTheme } from #98c379">"@material-ui/core/styles";
const theme = createMuiTheme();
const primary = theme.palette.primary.main; // use anywhere
2. Responsive grid without breakpoint boilerplate:
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={4}>...</Grid>
</Grid>
3. Conditional styles without className gymnastics:
<TableRow
style={{
backgroundColor: row.isOverdue ? #98c379">"rgba(239,68,68,0.08)" : undefined,
fontWeight: row.isPriority ? 700 : undefined,
}}
>
These patterns cut our average screen development time in half and made the codebase something a new developer could understand in a day instead of a week.