javascriptroom guide

React Coding Standards: Writing Clean and Maintainable Code

React has revolutionized front-end development with its component-based architecture, enabling developers to build dynamic, scalable applications. However, as projects grow in size and complexity, inconsistent code practices can lead to confusion, bugs, and maintenance headaches. **Coding standards**—guidelines for writing, formatting, and organizing code—are critical to ensuring consistency, readability, and collaboration across teams. In this blog, we’ll explore actionable React coding standards that will help you write clean, maintainable, and scalable code. Whether you’re a solo developer or part of a large team, these practices will streamline development, reduce technical debt, and make your codebase a joy to work with.

Table of Contents

  1. Naming Conventions
  2. Component Structure
  3. State Management
  4. Props Handling
  5. Hooks Best Practices
  6. Performance Optimization
  7. Error Handling
  8. Testing
  9. Code Organization
  10. Tooling for Enforcement
  11. Conclusion
  12. References

1. Naming Conventions

Consistent naming makes code self-documenting and easier to navigate. Follow these rules:

Components & Files

  • Use PascalCase for component names (e.g., UserProfile, OrderSummary).
  • Match component filenames to the component name (e.g., UserProfile.tsx for a component named UserProfile).
  • Avoid generic names like Component or Page—be specific (e.g., CheckoutPage instead of Page).

Example:

// UserProfile.tsx
import React from 'react';

const UserProfile: React.FC = () => {
  return <div>User Profile</div>;
};

export default UserProfile;

Functions & Variables

  • Use camelCase for functions, variables, and props (e.g., handleSubmit, userData).
  • Prefix event handlers with handle (e.g., handleClick, handleFormSubmit).
  • Use UPPER_SNAKE_CASE for constants (e.g., MAX_ITEMS, API_BASE_URL).

Example:

// Constants
const MAX_ITEMS = 10;

// Event handler
const handleDelete = (id: string) => {
  // logic to delete item
};

// Variable
const userSettings = { theme: 'dark' };

Booleans & Enums

  • Prefix booleans with is, has, should, or can (e.g., isActive, hasPermission).
  • Use enums for fixed sets of values (e.g., Status with Pending, Approved).

Example:

// Boolean
const isLoggedIn = true;
const hasAccess = false;

// Enum
enum Status {
  Pending = 'pending',
  Approved = 'approved',
  Rejected = 'rejected',
}
const orderStatus: Status = Status.Pending;

2. Component Structure

A well-structured component is easy to read and modify. Follow this order for clarity:

Typical Component Flow

  1. Imports: Start with external imports (React, libraries), then internal imports (components, utilities).
  2. Type Definitions: Interfaces/Types (for TypeScript) or PropTypes (for JavaScript).
  3. Constants: Static values used in the component.
  4. Helper Functions: Small, reusable logic (e.g., formatting dates, validating inputs).
  5. Component Function: The main component logic.
  6. Exports: Default or named exports.

Example (TypeScript):

// UserProfile.tsx
import React from 'react';
import { Avatar } from '../components/Avatar'; // Internal import
import { formatDate } from '../utils/date-utils'; // Helper utility

// Type definition
interface UserProfileProps {
  user: {
    id: string;
    name: string;
    joinDate: string;
  };
}

// Constants
const PROFILE_AVATAR_SIZE = 64;

// Helper function
const getMemberSince = (date: string) => `Member since ${formatDate(date)}`;

// Component
const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
  return (
    <div className="profile">
      <Avatar size={PROFILE_AVATAR_SIZE} name={user.name} />
      <h2>{user.name}</h2>
      <p>{getMemberSince(user.joinDate)}</p>
    </div>
  );
};

export default UserProfile;

Single Responsibility Principle

Each component should do one thing. If a component grows too large (e.g., >200 lines), split it into smaller, reusable components.

Bad:

// Too many responsibilities: rendering list + filtering + pagination
const UserList = () => {
  const [users, setUsers] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [page, setPage] = useState(1);

  // Fetch, filter, and paginate logic...

  return (
    <div>
      <input onChange={(e) => setSearchTerm(e.target.value)} />
      <ul>{filteredUsers.map(user => <li>{user.name}</li>)}</ul>
      <button onClick={() => setPage(page - 1)}>Prev</button>
      <button onClick={() => setPage(page + 1)}>Next</button>
    </div>
  );
};

Good:
Split into UserSearchInput, UserList, and Pagination components.

3. State Management

Effective state management prevents bugs and keeps logic predictable.

Local vs. Global State

  • Local State: Use useState for state specific to a component (e.g., form inputs, UI toggles).
  • Global State: Use Context API, Redux, or Zustand for state shared across components (e.g., user auth, theme settings).

Example: Local State

const LoginForm = () => {
  const [email, setEmail] = useState(''); // Local to LoginForm
  const [password, setPassword] = useState(''); // Local to LoginForm

  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      {/* ... */}
    </form>
  );
};

Avoid Redundant State

Derive state from existing state/props instead of duplicating it.

Bad:

const UserProfile = ({ user }) => {
  const [fullName, setFullName] = useState('');

  // Redundant: fullName can be derived from user.firstName + user.lastName
  useEffect(() => {
    setFullName(`${user.firstName} ${user.lastName}`);
  }, [user]);

  return <div>{fullName}</div>;
};

Good:

const UserProfile = ({ user }) => {
  const fullName = `${user.firstName} ${user.lastName}`; // Derived
  return <div>{fullName}</div>;
};

useReducer for Complex State

Use useReducer when state logic involves multiple sub-values or complex transitions (e.g., form validation with multiple steps).

Example:

type State = {
  step: number;
  formData: { name: string; email: string };
};

type Action =
  | { type: 'NEXT_STEP' }
  | { type: 'PREV_STEP' }
  | { type: 'UPDATE_FIELD'; payload: { name: string; value: string } };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    case 'PREV_STEP':
      return { ...state, step: state.step - 1 };
    case 'UPDATE_FIELD':
      return {
        ...state,
        formData: { ...state.formData, [action.payload.name]: action.payload.value },
      };
    default:
      return state;
  }
};

const MultiStepForm = () => {
  const [state, dispatch] = useReducer(reducer, {
    step: 1,
    formData: { name: '', email: '' },
  });

  return (
    <div>
      <input
        name="name"
        value={state.formData.name}
        onChange={(e) => dispatch({ type: 'UPDATE_FIELD', payload: { name: e.target.name, value: e.target.value } })}
      />
      <button onClick={() => dispatch({ type: 'NEXT_STEP' })}>Next</button>
    </div>
  );
};

4. Props Handling

Props are the primary way components communicate. Use these practices to keep props manageable.

Validate Props

  • TypeScript: Use interfaces/types to enforce prop shapes.
  • JavaScript: Use PropTypes for runtime validation.

Example (TypeScript):

interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary'; // Optional
  onClick: () => void; // Required function
}

const Button: React.FC<ButtonProps> = ({ label, variant = 'primary', onClick }) => {
  return <button className={`btn-${variant}`} onClick={onClick}>{label}</button>;
};

Example (JavaScript with PropTypes):

import PropTypes from 'prop-types';

const Button = ({ label, variant = 'primary', onClick }) => {
  return <button className={`btn-${variant}`} onClick={onClick}>{label}</button>;
};

Button.propTypes = {
  label: PropTypes.string.isRequired,
  variant: PropTypes.oneOf(['primary', 'secondary']),
  onClick: PropTypes.func.isRequired,
};

Destructure Props

Destructure props for readability, especially when passing many props.

Bad:

const UserCard = (props) => {
  return (
    <div>
      <h3>{props.user.name}</h3>
      <p>{props.user.email}</p>
      <button onClick={props.onEdit}>Edit</button>
    </div>
  );
};

Good:

const UserCard = ({ user, onEdit }) => {
  const { name, email } = user; // Nested destructuring
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
      <button onClick={onEdit}>Edit</button>
    </div>
  );
};

Avoid Prop Drilling

Passing props through multiple levels (“prop drilling”) leads to fragile code. Use Context API or composition instead.

Bad (Prop Drilling):

// App → Parent → Child → Grandchild (passing "theme" all the way)
const App = () => <Parent theme="dark" />;
const Parent = ({ theme }) => <Child theme={theme} />;
const Child = ({ theme }) => <Grandchild theme={theme} />;

Good (Context):

// ThemeContext.tsx
const ThemeContext = React.createContext('light');

const App = () => (
  <ThemeContext.Provider value="dark">
    <Parent />
  </ThemeContext.Provider>
);

const Grandchild = () => {
  const theme = useContext(ThemeContext); // Direct access
  return <div style={{ background: theme }} />;
};

5. Hooks Best Practices

Hooks are powerful, but misusing them causes bugs. Follow these rules:

Rules of Hooks

  • Only call hooks at the top level of components/custom hooks (not inside loops, conditionals, or nested functions).
  • Only call hooks from React components or custom hooks (not regular functions).

Bad:

const UserList = ({ users }) => {
  if (users.length === 0) {
    useState('No users'); // ❌ Inside conditional
  }
  // ...
};

Good:

const UserList = ({ users }) => {
  const [message, setMessage] = useState('');

  useEffect(() => {
    if (users.length === 0) {
      setMessage('No users'); // ✅ Inside effect (top-level)
    }
  }, [users]);
  // ...
};

Custom Hooks

Extract reusable logic into custom hooks (prefix with use).

Example: useLocalStorage

// useLocalStorage.ts
import { useState, useEffect } from 'react';

function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Usage in component
const [theme, setTheme] = useLocalStorage('theme', 'light');

Avoid Overusing Hooks

Too many hooks in one component make it hard to follow. Split the component or extract logic into a custom hook.

Bad:

const Dashboard = () => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);
  const [notifications, setNotifications] = useState([]);
  const [loading, setLoading] = useState(true);

  // 4 useEffect hooks for data fetching...
  useEffect(() => { /* fetch user */ }, []);
  useEffect(() => { /* fetch posts */ }, [user?.id]);
  useEffect(() => { /* fetch comments */ }, [posts]);
  useEffect(() => { /* fetch notifications */ }, [user?.id]);

  // ...
};

Good:
Extract data fetching into useUserData, usePostsData, etc., or split into UserDashboard, PostsDashboard components.

6. Performance Optimization

Optimize only when necessary, but know these tools:

Memoization

  • React.memo: Memoize components to prevent re-renders if props haven’t changed.
  • useMemo: Memoize expensive calculations (e.g., sorting large lists).
  • useCallback: Memoize functions passed as props to prevent unnecessary re-renders of child components.

Example: useCallback with React.memo

const Parent = () => {
  const [count, setCount] = useState(0);
  // Memoize handleClick to avoid recreating it on every render
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // Empty deps: stable reference

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child onClick={handleClick} /> {/* Child won't re-render */}
    </div>
  );
};

// Memoize Child to skip re-renders if props are the same
const Child = React.memo(({ onClick }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});

Lists: Keys and Virtualization

  • Always use stable, unique keys in lists (avoid indexes unless the list is static).
  • For long lists (>100 items), use virtualization (e.g., react-window or react-virtualized) to render only visible items.

Example: Key in Lists

// Bad: Index as key (risky if list is reordered/filtered)
{users.map((user, index) => (
  <UserItem key={index} user={user} />
))}

// Good: Unique ID as key
{users.map((user) => (
  <UserItem key={user.id} user={user} />
))}

7. Error Handling

Prevent crashes and improve UX with robust error handling.

Async Operations

Use try/catch with async logic (e.g., API calls in useEffect).

Example:

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const res = await fetch(`/api/users/${userId}`);
        if (!res.ok) throw new Error('Failed to fetch user');
        const data = await res.json();
        setUser(data);
      } catch (err) {
        setError(err.message); // Handle error
      }
    };

    fetchUser();
  }, [userId]);

  if (error) return <div>Error: {error}</div>; // Show error to user
  if (!user) return <div>Loading...</div>;

  return <div>{user.name}</div>;
};

Error Boundaries

Wrap components with error boundaries to catch errors and prevent app crashes.

Example: ErrorBoundary Component

class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true }; // Update state to show fallback UI
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('Error:', error, info); // Log error
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong. Please try again.</div>; // Fallback UI
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <UserProfile userId="123" />
</ErrorBoundary>

8. Testing

Tests ensure code works as expected and prevent regressions.

Unit Tests

Test individual components, hooks, and utilities with Jest and React Testing Library.

Example: Testing a Button Component

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('renders label and triggers onClick', () => {
  const handleClick = jest.fn();
  render(<Button label="Click Me" onClick={handleClick} />);

  const button = screen.getByText('Click Me');
  expect(button).toBeInTheDocument();

  fireEvent.click(button);
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Integration Tests

Test component interactions (e.g., form submission, data fetching).

Example: Testing a Login Form

// LoginForm.test.tsx
test('submits form with email and password', async () => {
  const mockSubmit = jest.fn();
  render(<LoginForm onSubmit={mockSubmit} />);

  // Fill inputs
  fireEvent.change(screen.getByLabelText(/email/i), { target: { value: '[email protected]' } });
  fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'password123' } });

  // Submit form
  fireEvent.click(screen.getByRole('button', { name: /login/i }));

  // Assert submit was called with correct data
  expect(mockSubmit).toHaveBeenCalledWith({
    email: '[email protected]',
    password: 'password123',
  });
});

9. Code Organization

A logical folder structure keeps code scalable and easy to navigate.

Feature-Based Structure

Group files by feature (e.g., auth, dashboard) instead of type (e.g., components, hooks).

Example:

src/
├── features/
│   ├── auth/
│   │   ├── components/ (LoginForm, RegisterForm)
│   │   ├── hooks/ (useLogin, useRegister)
│   │   ├── api.ts (auth API calls)
│   │   ├── types.ts (auth-related types)
│   │   └── AuthContext.tsx
│   ├── dashboard/
│   │   ├── components/ (StatsCard, RecentActivity)
│   │   └── DashboardPage.tsx
├── shared/ (reusable components/hooks used across features)
│   ├── components/ (Button, Input, Modal)
│   └── hooks/ (useLocalStorage, useApi)
└── App.tsx

Avoid Deep Nesting

Limit folder depth to 3–4 levels to avoid long import paths.

Bad:

src/components/user/profile/details/AddressForm.tsx

Good:

src/features/user/components/AddressForm.tsx

Index Files for Exports

Use index.ts files to simplify imports.

Example:

// features/auth/components/index.ts
export { default as LoginForm } from './LoginForm';
export { default as RegisterForm } from './RegisterForm';

// Usage in another file
import { LoginForm, RegisterForm } from '../features/auth/components';

10. Tooling for Enforcement

Automate standards with tools to reduce manual effort.

Linting & Formatting

  • ESLint: Enforce code quality rules (e.g., no unused variables, hook rules).
  • Prettier: Auto-format code for consistency (indentation, line length).

Setup:

  1. Install dependencies:
    npm install eslint prettier eslint-config-prettier eslint-plugin-prettier --save-dev
  2. Configure .eslintrc.js:
    module.exports = {
      extends: [
        'react-app', // Base React config
        'plugin:react-hooks/recommended', // Hook rules
        'prettier', // Disable ESLint formatting rules
      ],
      rules: {
        'react/prop-types': 'error', // Enforce prop types
        'no-console': 'warn', // Warn on console logs
      },
    };
  3. Add scripts to package.json:
    "scripts": {
      "lint": "eslint .",
      "format": "prettier --write ."
    }

Pre-Commit Hooks

Use Husky and lint-staged to run linting/formatting before commits.

Setup:

npm install husky lint-staged --save-dev
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"

Add to package.json:

"lint-staged": {
  "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.{json,md}": ["prettier --write"]
}

TypeScript

Use TypeScript for type safety, catching errors at compile time.

Example: Type Safety

// Type error caught at compile time
interface User {
  id: string;
  name: string;
}

const user: User = { id: 123 }; // ❌ Type 'number' is not assignable to type 'string' (id)

Conclusion

Writing clean, maintainable React code isn’t about perfection—it’s about consistency and empathy for your team (and future self). By following these standards—consistent naming, structured components, smart state management, and leveraging tools—you’ll build applications that are easy to debug, scale, and collaborate on.

Remember: standards evolve. Regularly review and update your practices as your team and project grow.

References