Frontend Standards
Validation
Use Yup for frontend validation schemas:
import * as Yup from "yup";
export const userValidationSchema = Yup.object().shape({
email: Yup.string().email("Invalid email").required("Email is required"),
age: Yup.number()
.min(18, "Must be at least 18 years old")
.required("Age is required"),
password: Yup.string()
.min(6, "Password must be at least 6 characters")
.required("Password is required"),
});
Component Composition
Do not create a page file that only renders a single child component. Render the actual page content directly.
Example:
// Bad
const UsersPage = () => <UsersList />;
// Good
const UsersPage = () => (
<div>
<PageHeader title="Users" />
<UsersList />
</div>
);
JSX Conditions
Avoid unclear inline conditions. Define clear boolean flags first:
const shouldShowSelectEmailField = !isEdit && !token;
return shouldShowSelectEmailField ? <SelectEmailField /> : null;
Name Your Effects
Always use named function expressions inside useEffect instead of anonymous arrow functions. This is a zero-cost syntax change that improves readability, debugging, and code quality.
Before (anonymous — avoid):
useEffect(() => {
document.title = `${count} items`;
}, [count]);
After (named — preferred):
useEffect(
function updateDocumentTitle() {
document.title = `${count} items`;
},
[count],
);
Why this matters
- Readability: You can skim effect names to understand a component's data flow without reading each implementation.
- Debugging: Stack traces, Sentry, and React DevTools show
at connectToWebSocket @ Component.tsx:14instead ofat (anonymous) @ Component.tsx:14. - Refactoring signal: If you struggle to name an effect without using "and" (e.g.
syncWidthAndApplyTheme), it's doing too much — split it into separate effects. - Detects effects that shouldn't exist: If the best name sounds like internal state shuffling, the code probably belongs somewhere else (derived value or event handler).
Name cleanup functions too
When cleanup does non-trivial work, name the return function for symmetry:
useEffect(
function pollServerForUpdates() {
const intervalId = setInterval(() => {
fetch(`/api/status/${serverId}`)
.then((res) => res.json())
.then(setServerStatus);
}, 5000);
return function stopPollingServer() {
clearInterval(intervalId);
};
},
[serverId],
);
Effects that shouldn't be effects
Naming makes these anti-patterns obvious:
Derived state — compute during render instead:
// Bad: extra render cycle for no reason
useEffect(
function syncFullName() {
setFullName(`${firstName} ${lastName}`);
},
[firstName, lastName],
);
// Good: derive it directly
const fullName = `${firstName} ${lastName}`;
Event responses — put logic in the handler instead:
// Bad: indirection through state + effect
useEffect(
function resetFormOnSubmit() {
if (submitted) {
setName("");
setEmail("");
setSubmitted(false);
}
},
[submitted],
);
// Good: handle it where the event happens
function handleSubmit() {
submitForm({ name, email });
setName("");
setEmail("");
}
When to extract to a custom hook vs keep inline
- Extract if the effect manages its own state AND might be reused across components.
- Keep inline if it's a single-use effect with no associated state.
- Even inside custom hooks, still name the effect:
function useWindowWidth() {
const [width, setWidth] = useState(
typeof window !== "undefined" ? window.innerWidth : 0,
);
useEffect(function trackWindowWidth() {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
Don't over-abstract — components growing longer as they do more is normal. Not every piece of logic needs extraction.
Good effect names for reference
Effects with clear, concrete names tend to be legitimate synchronization points:
connectToWebSocket— event-based synchronizationsubscribeToGeolocation— continuous external system syncinitializeMapInstance— one-time third-party setupsynchronizeViewportWithState— keeping an external system in sync with React state
Reference: Name Your Effects by Dan Neciu
Redux Usage
- Do not import internal slice actions directly in components.
- Export wrapper functions that call internal actions.
- Keep slice actions internal to slices.
- Avoid overpowered actions. Prefer intent-specific actions that operate inside the slice.
Example:
// slice.ts
const bookingSlice = createSlice({
name: "booking",
reducers: {
setBookingCriteria(state, action) {
state.criteria = { ...state.criteria, ...action.payload };
},
},
});
export const updateBookingCriteria = (payload) => (dispatch, getState) => {
dispatch(bookingSlice.actions.setBookingCriteria(payload));
};