By Borifan Dabasa | Full Stack Developer
As a MERN stack developer who's built multiple production applications, I've learned that writing React code is easy—writing scalable React code is an art. Today, I'm sharing the architecture patterns and best practices I use in every project.
The Problem with "Just Start Coding"
When I built my first React app, I threw everything into components without thinking about structure. Fast forward three months, and I was drowning in prop drilling, duplicate logic, and components that did way too much.
Sound familiar?
1. Folder Structure That Scales
Here's the structure I use for all my React projects now:
src/
├── components/
│ ├── common/ # Reusable UI components
│ ├── layout/ # Layout components
│ └── features/ # Feature-specific components
├── hooks/ # Custom hooks
├── context/ # Context providers
├── services/ # API calls
├── utils/ # Helper functions
├── constants/ # Constants and configs
└── pages/ # Page components
Why this works: Each folder has a single responsibility. When I need to find something, I know exactly where to look.
2. Component Composition Over Complexity
I learned this the hard way. Here's a component from my early days:
// ❌ Bad: God component doing everything
function UserDashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
// 200 lines of logic...
return (
<div>
{/* 300 lines of JSX */}
</div>
);
}
Now I break it down:
// ✅ Good: Composed components
function UserDashboard() {
return (
<DashboardLayout>
<UserProfile />
<UserStats />
<UserPosts />
</DashboardLayout>
);
}
Rule of thumb: If your component is over 150 lines, it's probably doing too much.
3. Custom Hooks for Logic Reuse
Custom hooks changed my life. Instead of copying logic between components, I extract it:
// hooks/useAuth.js
export function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(setUser);
setLoading(false);
return unsubscribe;
}, []);
return { user, loading };
}
// Now use it anywhere
function Profile() {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
I use custom hooks for:
- API calls (
useFetch,useApi) - Form handling (
useForm) - Local storage (
useLocalStorage) - Debouncing (
useDebounce)
4. State Management: Keep It Simple
I see developers reaching for Redux immediately. Here's my approach:
- Local state for component-specific data
- Context for app-wide data (theme, auth)
- Redux/Zustand only when Context becomes messy
For my e-commerce project, I used Context for cart and auth. That's it. No Redux needed.
// context/CartContext.js
export function CartProvider({ children }) {
const [cart, setCart] = useState([]);
const addToCart = (item) => {
setCart([...cart, item]);
};
return (
<CartContext.Provider value={{ cart, addToCart }}>
{children}
</CartContext.Provider>
);
}
5. Performance Optimization
Three techniques I use in every project:
Code Splitting
// Lazy load heavy components
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}
Memoization
// Prevent unnecessary re-renders
const ExpensiveComponent = memo(({ data }) => {
return <div>{/* Heavy computation */}</div>;
});
Virtual Lists
For my crypto tracker with 1000+ coins, I use react-window:
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={coins.length}
itemSize={50}
>
{CoinRow}
</FixedSizeList>
6. Error Boundaries
This saved me during production bugs:
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
// Wrap your app
<ErrorBoundary>
<App />
</ErrorBoundary>
7. Environment Variables
Never hardcode API URLs:
// .env
REACT_APP_API_URL=https://api.example.com
REACT_APP_API_KEY=your_key_here
// Use in code
const API_URL = process.env.REACT_APP_API_URL;
Real-World Example: My Blog Project
In my Next.js blog (live at borifan-blog.vercel.app), I implemented:
- File-based routing for automatic code splitting
- ISR (Incremental Static Regeneration) for fast loads
- TypeScript for type safety
- Tailwind CSS for consistent styling
Result? Lighthouse score of 95+ and sub-second load times.
Key Takeaways
- Structure matters - Organize by feature, not file type
- Compose, don't complicate - Small, focused components
- Extract logic - Custom hooks are your friend
- Optimize smartly - Measure before optimizing
- Plan for errors - Error boundaries save production
What's Next?
I'm currently exploring:
- Server Components in Next.js 14
- React Query for better data fetching
- Micro-frontends for large apps
Want to see these patterns in action? Check out my projects on GitHub.
Questions? Drop a comment or reach out at dabasaborifan@gmail.com
Connect with me:
- GitHub: @Borifan02
- LinkedIn: Borifan Dabasa
Top comments (0)