“The primary feature for easy maintenance is locality: Locality is that characteristic of source code that enables a programmer to understand that source by looking at only a small portion of it.
– Richard Gabriel
– Carson Gross
– Nate Weller
What is Locality of Behavior?
In short: The behavior of a unit of code should be as obvious as possible by looking only at that unit of code.
This sounds deceptively simple, but it’s violated constantly in modern frontend codebases. As applications grow in complexity, the distance between a component’s appearance and its actual behavior tends to increase.
The Tale of Two Buttons
Consider these two implementations of a button that makes an AJAX request:
Example 1 (htmx):
<button hx-get="/clicked">Click Me</button>
Example 2 (JS):
<button id="d1">Click Me</button>
document.getElementById('d1').addEventListener('click', () => {
fetch('/clicked', {
method: 'GET',
})
.then(response => response.json())
.then(data => {
// Handle response data
});
});
In the first example, the button’s behavior is immediately obvious when looking at the element itself. In the second, understanding what happens when clicking requires finding the corresponding JavaScript that might live anywhere in your application.
This distinction might seem minor for a small application, but as your codebase grows to thousands of components, the ability to quickly understand behavior becomes critical.
LoB in Component Architecture
Modern frontend frameworks have increasingly embraced component-based architectures, which provide natural boundaries for implementing locality. A well-designed React, Vue, or Angular component encapsulates everything needed to understand its behavior:
- Template/HTML structure
- Logic/JavaScript behavior
- Styling/CSS presentation
- Tests for verification
Co-locating these elements together brings benefits:
- Maintainability: Changes to one aspect of a component don’t require hunting through multiple files
- Discoverability: New team members can understand a component by examining a single location
- Reduced cognitive load: Developers can hold the complete behavior in mind without context switching
I’ve found the most maintainable components are those where I can understand their complete behavior by examining a single file or directory, without needing to trace behavior through indirect connections across the codebase.
State Management Through the LoB Lens
Locality is likewise highly applicable to state management. State that affects a component’s behavior should ideally live as close as possible to that component.
Early React emphasized lifting state upward, but this often created situations where changes in one component would unexpectedly affect another. The pendulum has swung back toward local state with hooks, which make it easy to keep behavior tightly bound to components.
For example, compare these approaches:
Distant state:
// In a global store file
const globalState = {
userPreferences: {
theme: 'dark',
fontSize: 'medium'
}
};
// In a component file far, far away
function UserCard() {
// Where does this come from? What else can change it?
const { theme } = useGlobalState();
return <div className={`card ${theme}`}>...</div>;
}
Local state with clear escalation paths:
function UserCard({ theme }) {
// State needed only by this component stays here
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className={`card ${theme}`}>
<button onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Show Less' : 'Show More'}
</button>
{isExpanded && <div>Extra content here</div>}
</div>
);
}
The second approach makes behavior immediately obvious by keeping state close to where it’s used, while still allowing shared state to be passed explicitly.
Common Anti-patterns and Solutions
- Event Bus Abuse: Using a global event bus where components emit and listen for events without clear connections
- CSS Class Treasure Hunts: Component appearance determined by distant CSS selectors
- Utility File Sprawl: Extracting any potentially reusable code into utility files, creating a maze of dependencies
- Prop Drilling: Passing props through multiple component layers, obscuring the data flow
The solution usually involves restructuring to bring behavior closer to the components affected:
- Replace event buses with explicit props and callbacks
- Use CSS-in-JS or scoped CSS to keep styles with components
- Keep utility functions with the components that use them until there’s clear evidence of broader reuse
- Use composition to avoid excessive prop drilling
Balancing LoB with Other Principles
LoB will inevitably conflict with other software principles. Two important tensions to manage:
LoB vs. DRY (Don’t Repeat Yourself)
Absolute adherence to DRY can lead to premature abstraction, pulling related behavior apart. I’ve learned to tolerate strategic duplication when it improves locality. As the saying goes: “Duplication is far cheaper than the wrong abstraction.”
LoB vs. SoC (Separation of Concerns)
Traditional Separation of Concerns organizes code by technical type (HTML, CSS, JS), but component architecture reorganizes around functional concerns instead. This is why inline styles and CSS-in-JS have gained popularity despite initially seeming to violate SoC. They improve locality by keeping related concerns together.
The key is finding the right level of abstraction where components remain coherent, self-contained units without becoming monolithic.
Practical Implementation Strategies
Here are strategies I’ve found effective for improving locality in frontend codebases:
- Default to co-location: Start with everything related to a component in one file or directory. Extract only when complexity demands it.
- Use composition over configuration: Make component behavior explicit through composition rather than through complex configuration objects.
- Implement “Screaming Architecture”: Name and organize files so their purpose is immediately obvious, not by technical type but by domain feature.
- Document the unexpected: When behavior can’t be made obvious through code structure, document it explicitly where the component is defined.
- Make state transitions visible: Use state machines or explicit state transitions to make behavior changes obvious.
Real-world Benefits
Teams I’ve led that embraced locality have seen significant improvements:
- Faster onboarding: New developers could become productive within days rather than weeks
- Reduced “tribal knowledge”: Less reliance on “ask Sarah, she wrote that component”
- More confident refactoring: Changes could be made with greater confidence that unexpected side effects wouldn’t emerge
- Easier code reviews: Reviewers could understand changes without extensive context
Conclusion
Locality of Behavior isn’t just another software principle—it’s about creating humane systems that respect cognitive limitations. By keeping related code together and making behavior obvious, we build codebases that remain maintainable as they grow.