I Stopped Letting GitHub Copilot Invent My React Standard. Here Is What I Did Instead.
Let's be honest, GitHub Copilot is magic. It's like having a coding wizard peering over your shoulder, anticipating your next move, sometimes even reading your mind. For repetitive tasks, boilerplate, or just speeding through a tricky algorithm, it's an unparalleled productivity booster. I've found myself marveling at its suggestions countless times.
But here's the thing: while Copilot is fantastic at generating code, it's not designed to generate standards. And in the world of professional React development, consistency isn't just a nice-to-have; it's the bedrock of maintainability, scalability, and team velocity.
The Subtle Drift: My "Aha!" Moment
For a while, I noticed a subtle but growing inconsistency in our codebase. Two different developers, tasked with similar features, would implement their components with entirely different folder structures, naming conventions, or prop-passing patterns. Both would often use Copilot, and Copilot, being the helpful AI it is, would offer perfectly valid, yet often divergent, solutions based on the immediate context or patterns it had learned.
My "aha!" moment came during a code review. I saw a component named PrimaryButton.tsx in one part of the app, and ButtonComponent.tsx in another, both serving similar purposes. One used interface for props, the other type. One useState where another used useReducer for simpler state. It wasn't "wrong" code; it was just different code. Multiply this by dozens of components and several developers, and suddenly our once-clean project started feeling like a patchwork quilt.
This wasn't Copilot's fault. It was ours. We were implicitly letting an AI define our patterns, rather than explicitly defining them ourselves. Copilot is an amazing accelerator of existing patterns, but it's a poor inventor of unique, team-specific architectural standards.
Why Standards Matter More Than You Think
Before we dive into what I did, let's quickly touch on why this matters. In my experience, a well-defined React standard:
- Accelerates Onboarding: New team members can hit the ground running because they don't have to learn a new coding style for every file.
- Reduces Cognitive Load: Developers spend less time figuring out "how should I do this?" and more time solving the actual business problem.
- Improves Code Readability & Maintainability: Consistent code is easier to read, debug, and refactor.
- Streamlines Code Reviews: Reviews become focused on logic and functionality, not stylistic nitpicks.
- Fosters Team Cohesion: Everyone feels like they're contributing to a unified vision, not just their own corner of the codebase.
What We Did Instead: Defining Our React Standard
We recognized that the solution wasn't to ditch Copilot (heaven forbid!), but to turn it into an enforcer and accelerator of our human-defined standards. Here's a peek at some key areas we focused on:
1. Establishing a Clear Folder Structure
This is often the first thing new developers encounter, and it sets the tone. We moved away from just src/components being a dumping ground.
src/
├── app/ // Top-level layout, routing, app-wide context
│ ├── layout.tsx
│ └── router.tsx
├── components/ // Reusable UI primitives (buttons, inputs, cards)
│ ├── Button/
│ │ ├── Button.tsx
│ │ └── Button.test.tsx
│ ├── Input/
│ └── ...
├── features/ // Domain-specific features (e.g., Auth, UserProfile, ProductList)
│ ├── Auth/
│ │ ├── components/ // Feature-specific components
│ │ ├── hooks/
│ │ ├── services/
│ │ └── AuthPage.tsx
│ ├── UserProfile/
│ └── ...
├── hooks/ // Reusable logic hooks (useDebounce, useLocalStorage)
├── services/ // API interactions, data fetching
├── utils/ // Helper functions
├── types/ // Global TypeScript types/interfaces
└── main.tsx
- Insight: The "feature-first" approach for
features/helps encapsulate domain logic, making features easier to extract or scale. Generic UI elements live incomponents/.
2. Naming Conventions & File Organization
Consistency here is a huge win for readability.
- Components & Files: Always
PascalCase(e.g.,MyComponent.tsx). If a component has multiple related files (e.g.,Button.tsx,Button.styles.ts,Button.test.tsx), they live in a dedicatedButton/folder. - Props & Variables:
camelCase(e.g.,userName,onClick). - Custom Hooks: Always
usePrefix(e.g.,useAuth,useDebounce). - TypeScript Types/Interfaces:
PascalCasewithIprefix for interfaces (e.g.,IUserProps) or justPascalCasefor types (e.g.,UserType). Our team agreed onPascalCasefor types, avoiding theIprefix for simplicity.
3. Component Design Patterns
This is where the real architectural muscle comes in.
- Presentational vs. Container (Simplified Atomic Design): We adopted a simplified Atomic Design principle.
components/holds "atoms" (buttons, icons) and "molecules" (input fields with labels).features/often contain "organisms" (complex UI sections) and "templates" (page layouts).- Insight: This helps prevent prop drilling by encouraging containers to fetch data and pass minimal, already-processed props to presentational components.
-
Compound Components: For complex components like
TabsorDropdowns, we standardized on compound components.
// Bad (too many props, rigid) <Tabs activeTab={currentTab} onTabChange={handleTabChange} tabTitles={['Overview', 'Details']} tabContents={[<OverviewContent />, <DetailsContent />]} /> // Good (flexible, declarative) <Tabs value={currentTab} onChange={handleTabChange}> <Tabs.List> <Tabs.Trigger value="overview">Overview</Tabs.Trigger> <Tabs.Trigger value="details">Details</Tabs.Trigger> </Tabs.List> <Tabs.Content value="overview"> <OverviewContent /> </Tabs.Content> <Tabs.Content value="details"> <DetailsContent /> </Tabs.Content> </Tabs>- Benefit: They provide a clear API and strong type-checking with TypeScript, enhancing discoverability and preventing prop-hell.
4. Prop Handling with TypeScript
TypeScript is non-negotiable for us. Standardizing prop definitions is crucial.
- Always Type Props: Every component must define its props using an
interfaceortype. - Destructuring: Destructure props immediately at the component's entry point.
- Default Props: Use ES6 default parameters where appropriate.
interface ButtonProps {
variant?: 'primary' | 'secondary';
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
isDisabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
onClick,
children,
isDisabled = false,
}) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={isDisabled}
>
{children}
</button>
);
};
5. When to Use Which State Management Approach
While we use Zustand for global state, the standard dictated when to use useState, useReducer, or useContext for local or component-tree state.
-
useState: For simple, independent state within a component. -
useReducer: For complex state logic involving multiple sub-states or transitions, especially when the next state depends on the previous one. -
useContext: For sharing state or functions between components without prop drilling, but primarily within a defined sub-tree (e.g., a feature context).- Insight: Don't reach for global state management for everything. Often, local state or
useReducer/useContextis sufficient and simpler.
- Insight: Don't reach for global state management for everything. Often, local state or
6. Error Boundaries
We established a consistent way to implement and deploy React Error Boundaries at key points in our application to catch UI errors gracefully.
Beyond the Rules: Documentation and Buy-in
It's not enough to just have rules.
- Document the "Why": We created a living document (a simple Markdown file in our repo) explaining not just what the standard is, but why we chose it. This fosters understanding and makes it easier for the team to adapt and evolve the standard.
- Team Buy-in: This wasn't a top-down mandate. We discussed, debated, and collectively agreed on these standards. Everyone had a voice, which led to greater adoption and ownership.
- Iterate, Don't Dictate: Our standard isn't set in stone. We periodically review and adjust it based on new learnings, React updates, or team feedback.
- Leverage Linters and Formatters: Tools like ESLint and Prettier are invaluable. Once we had our human-defined standard, we configured these tools to automatically enforce as much of it as possible. This is where Copilot really shines again — its suggestions can now align with our configured linter rules, acting as a highly intelligent assistant for our established standard.
The Payoff
The shift has been transformative. Our codebase is now significantly more predictable. Onboarding new developers is faster. Code reviews are smoother, focusing on logic rather than style. We still use GitHub Copilot extensively, but now it's helping us build a cohesive, well-structured application according to our rules, not just its general knowledge of "a way" to write React.
It's about empowering your team with clarity, so they can focus their creative energy on solving problems, rather than wrestling with arbitrary stylistic choices. Define your standard, document it, get team buy-in, and then let your intelligent tools truly accelerate your progress.
✨ Let's keep the conversation going!
If you found this interesting, I'd love for you to check out more of my work or just drop in to say hello.
✍️ Read more on my blog: bishoy-bishai.github.io
☕ Let's chat on LinkedIn: linkedin.com/in/bishoybishai
Top comments (0)