Skip to main content

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:14 instead of at (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 synchronization
  • subscribeToGeolocation — continuous external system sync
  • initializeMapInstance — one-time third-party setup
  • synchronizeViewportWithState — 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));
};