50 React Development Rules with Examples

Component Design & Architecture

1. Use functional components over class components

Bad:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Good:

function Welcome({ name }) {
  return <h1>Hello, {name}</h1>;
}

2. Keep components small and focused

Bad:

function UserDashboard({ user }) {
  return (
    <div>
      <div>{user.name}</div>
      <div>{user.email}</div>
      <div>
        {user.posts.map(post => (
          <div key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
            <span>{post.date}</span>
            <button onClick={() => deletePost(post.id)}>Delete</button>
          </div>
        ))}
      </div>
      <form onSubmit={handleSubmit}>
        <input value={title} onChange={e => setTitle(e.target.value)} />
        <textarea value={content} onChange={e => setContent(e.target.value)} />
        <button type="submit">Add Post</button>
      </form>
    </div>
  );
}

Good:

function UserDashboard({ user }) {
  return (
    <div>
      <UserProfile user={user} />
      <PostList posts={user.posts} />
      <PostForm onSubmit={handleAddPost} />
    </div>
  );
}

3. Use PascalCase for component names

Bad:

function userCard() { /* ... */ }
function user_profile() { /* ... */ }

Good:

function UserCard() { /* ... */ }
function UserProfile() { /* ... */ }

4. Create custom hooks for reusable logic

Bad:

// Repeated in multiple components
function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

Good:

// Custom hook
function useUser() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  return { user, loading, error };
}

// Component using the hook
function UserProfile() {
  const { user, loading, error } = useUser();
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

5. Prefer composition over inheritance

Bad:

class BaseButton extends React.Component {
  render() {
    return <button className="btn">{this.props.children}</button>;
  }
}

class PrimaryButton extends BaseButton {
  render() {
    return <button className="btn btn-primary">{this.props.children}</button>;
  }
}

Good:

function Button({ variant = 'default', children, ...props }) {
  return (
    <button 
      className={`btn btn-${variant}`} 
      {...props}
    >
      {children}
    </button>
  );
}

// Usage
<Button variant="primary">Click me</Button>

6. Use TypeScript for better type safety

JavaScript:

function UserCard({ user }) {
  return <div>{user.name}</div>; // No type checking
}

TypeScript:

interface User {
  id: number;
  name: string;
  email: string;
}

function UserCard({ user }: { user: User }) {
  return <div>{user.name}</div>; // Type checked!
}

7. Single responsibility principle

Bad:

function UserComponent({ userId }) {
  // Fetching data
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // Formatting data
  const formatDate = (date) => new Date(date).toLocaleDateString();

  // Handling actions
  const handleEdit = () => { /* edit logic */ };
  const handleDelete = () => { /* delete logic */ };

  // Rendering everything
  return (
    <div>
      <img src={user?.avatar} />
      <h2>{user?.name}</h2>
      <p>Joined: {formatDate(user?.joinDate)}</p>
      <button onClick={handleEdit}>Edit</button>
      <button onClick={handleDelete}>Delete</button>
    </div>
  );
}

Good:

// Separate concerns into different components/hooks
function useUser(userId) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  return user;
}

function UserAvatar({ src, alt }) {
  return <img src={src} alt={alt} />;
}

function UserActions({ onEdit, onDelete }) {
  return (
    <div>
      <button onClick={onEdit}>Edit</button>
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}

function UserCard({ userId, onEdit, onDelete }) {
  const user = useUser(userId);
  
  return (
    <div>
      <UserAvatar src={user?.avatar} alt={user?.name} />
      <h2>{user?.name}</h2>
      <p>Joined: {new Date(user?.joinDate).toLocaleDateString()}</p>
      <UserActions onEdit={onEdit} onDelete={onDelete} />
    </div>
  );
}

8. Use proper file and folder structure

Bad:

src/
  components/
    everything.js
    all-components.js

Good:

src/
  components/
    User/
      UserCard.jsx
      UserProfile.jsx
      UserList.jsx
      index.js
    Product/
      ProductCard.jsx
      ProductDetails.jsx
      index.js
  hooks/
    useUser.js
    useProducts.js
  utils/
    dateHelpers.js
    apiHelpers.js

State Management

9. Use useState for local component state

Example:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
}

10. Use useReducer for complex state logic

Bad (useState for complex state):

function ShoppingCart() {
  const [items, setItems] = useState([]);
  const [total, setTotal] = useState(0);
  const [discount, setDiscount] = useState(0);
  const [loading, setLoading] = useState(false);

  const addItem = (item) => {
    setItems(prev => [...prev, item]);
    setTotal(prev => prev + item.price);
  };

  const removeItem = (id) => {
    const item = items.find(i => i.id === id);
    setItems(prev => prev.filter(i => i.id !== id));
    setTotal(prev => prev - item.price);
  };

  // More complex logic...
}

Good (useReducer):

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.item],
        total: state.total + action.item.price
      };
    case 'REMOVE_ITEM':
      const item = state.items.find(i => i.id === action.id);
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.id),
        total: state.total - item.price
      };
    case 'SET_LOADING':
      return { ...state, loading: action.loading };
    default:
      return state;
  }
}

function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, {
    items: [],
    total: 0,
    discount: 0,
    loading: false
  });

  const addItem = (item) => dispatch({ type: 'ADD_ITEM', item });
  const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', id });
}

11. Lift state up when multiple components need it

Bad:

function App() {
  return (
    <div>
      <UserProfile /> {/* Has its own user state */}
      <UserSettings /> {/* Has its own user state */}
    </div>
  );
}

Good:

function App() {
  const [user, setUser] = useState(null);

  return (
    <div>
      <UserProfile user={user} />
      <UserSettings user={user} onUserUpdate={setUser} />
    </div>
  );
}

12. Avoid unnecessary re-renders

Bad:

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveComponent /> {/* Re-renders every time count changes */}
    </div>
  );
}

Good:

const MemoizedExpensiveComponent = React.memo(ExpensiveComponent);

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <MemoizedExpensiveComponent /> {/* Only re-renders when its props change */}
    </div>
  );
}

13. Initialize state correctly

Bad:

function UserProfile({ userId }) {
  // Function called on every render!
  const [user, setUser] = useState(fetchUserSync(userId));
}

Good:

function UserProfile({ userId }) {
  // Lazy initial state - function called only once
  const [user, setUser] = useState(() => getInitialUser(userId));
  
  // Or better yet, use useEffect for async operations
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
}

14. Use state updater functions for dependent updates

Bad:

function Counter({ increment = 1 }) {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + increment); // Stale closure problem
    setCount(count + increment); // Won't work as expected
  };
}

Good:

function Counter({ increment = 1 }) {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(prev => prev + increment);
    setCount(prev => prev + increment); // Works correctly
  };
}