DEV Community

Borifan Dabasa
Borifan Dabasa

Posted on

Building Scalable React Applications: Lessons from Real Projects

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now I break it down:

// ✅ Good: Composed components
function UserDashboard() {
  return (
    <DashboardLayout>
      <UserProfile />
      <UserStats />
      <UserPosts />
    </DashboardLayout>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Local state for component-specific data
  2. Context for app-wide data (theme, auth)
  3. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Memoization

// Prevent unnecessary re-renders
const ExpensiveComponent = memo(({ data }) => {
  return <div>{/* Heavy computation */}</div>;
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

  1. Structure matters - Organize by feature, not file type
  2. Compose, don't complicate - Small, focused components
  3. Extract logic - Custom hooks are your friend
  4. Optimize smartly - Measure before optimizing
  5. 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:

React #WebDevelopment #JavaScript #Frontend #MERN

Top comments (0)