Clean Code: Declarative Programming

Imagine you're in a taxi. You can either give the driver turn-by-turn directions ("take the next left, then the third right..."), or you can just tell them your destination. The first approach is imperative: you're describing the steps to get there. The second is declarative: you're describing the desired result, and you trust the driver to handle the details.

Most code starts with imperative instructions. React and JavaScript's array methods make the alternative familiar: describe the result and leave the mechanics to the implementation.

From Imperative Loops to Declarative Chains

A useful example is transforming an array. A for loop spells out each operation; filter and map describe the result directly.

// Bad: The imperative approach describes every step of the process.
function getActiveUserNames(users: User[]): string[] {
  const activeUsers = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].isActive) {
      activeUsers.push(users[i].name);
    }
  }
  return activeUsers;
}

// Good: The declarative approach describes the result we want.
function getActiveUserNames(users: User[]): string[] {
  return users
    .filter(user => user.isActive)
    .map(user => user.name);
}

The declarative version says which users I want and which value to return. The loop makes the reader follow the mechanics instead.

Declarative UIs with React

Modern UI libraries like React are built around the idea of declarative programming. You describe what the UI should look like for a given state, and React takes care of the "how" by updating the DOM efficiently.

// Bad: An imperative-style React component. We're manually managing the filtered list.
function UserList({ users }: { users: User[] }) {
  const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    const filtered = [];
    for (let i = 0; i < users.length; i++) {
      if (users[i].name.toLowerCase().includes(searchTerm.toLowerCase())) {
        filtered.push(users[i]);
      }
    }
    setFilteredUsers(filtered);
  }, [users, searchTerm]);

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {filteredUsers.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

// Good: A declarative React component. We describe what `filteredUsers` should be, and React handles the rest.
function UserList({ users }: { users: User[] }) {
  const [searchTerm, setSearchTerm] = useState('');

  // We declare that filteredUsers is the users array filtered by the search term.
  const filteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {/* We just render the filtered list. React handles the updates. */}
      {filteredUsers.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

The declarative approach often leads to code that is more predictable and easier to debug because you're not managing all the nitty-gritty state changes yourself.

Writing declarative code is a shift in mindset. It's about focusing on the end result and letting the tools and abstractions handle the implementation details. This often leads to code that is cleaner and easier to maintain.